Skip to main content

oxirs_core/query/
property_function_registry.rs

1//! # SPARQL 1.2 Property Function Registry
2//!
3//! Implements an extensible property function framework analogous to Jena's
4//! `PropertyFunctionFactory`. Property functions are magic predicates that,
5//! when encountered as the predicate of a triple pattern, trigger custom
6//! evaluation logic instead of a normal triple-match.
7//!
8//! ## Overview
9//!
10//! Standard SPARQL triple patterns like `?s :p ?o` are matched against the
11//! stored graph. A **property function** overrides that: when the predicate
12//! IRI is registered in the `PropertyFunctionRegistry`, the engine delegates
13//! evaluation to the registered `PropertyFunction` implementation.
14//!
15//! ### Use Cases
16//!
17//! - Full-text search: `?s text:search ("rust programming" 10)`
18//! - Spatial queries: `?s geo:nearby (51.5 -0.12 1000)`
19//! - List operations: `?list list:member ?item`
20//! - Custom aggregations: `?s custom:topK (?score 5)`
21//!
22//! ## Architecture
23//!
24//! ```text
25//! PropertyFunctionRegistry
26//!   ├─ register(iri, factory)
27//!   ├─ lookup(iri) -> Option<PropertyFunction>
28//!   └─ built-in property functions
29//!        ├─ list:member
30//!        ├─ list:index
31//!        ├─ list:length
32//!        ├─ text:search
33//!        ├─ apf:splitIRI
34//!        └─ apf:str (string decomposition)
35//! ```
36
37use crate::model::Term;
38use crate::OxirsError;
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41use std::fmt;
42use std::sync::Arc;
43
44/// Represents the subject side of a property function triple pattern.
45/// Can be a single variable/term or a list of arguments.
46#[derive(Debug, Clone, PartialEq)]
47pub enum PropertyFunctionArg {
48    /// A single RDF term
49    Term(Term),
50    /// A variable name (without the ? prefix)
51    Variable(String),
52    /// An argument list (for multi-arg property functions)
53    List(Vec<PropertyFunctionArg>),
54}
55
56impl fmt::Display for PropertyFunctionArg {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            PropertyFunctionArg::Term(t) => write!(f, "{t:?}"),
60            PropertyFunctionArg::Variable(v) => write!(f, "?{v}"),
61            PropertyFunctionArg::List(args) => {
62                write!(f, "(")?;
63                for (i, a) in args.iter().enumerate() {
64                    if i > 0 {
65                        write!(f, " ")?;
66                    }
67                    write!(f, "{a}")?;
68                }
69                write!(f, ")")
70            }
71        }
72    }
73}
74
75/// A single binding row produced by a property function evaluation.
76#[derive(Debug, Clone, PartialEq)]
77pub struct PropertyFunctionBinding {
78    /// Variable-to-term bindings
79    bindings: HashMap<String, Term>,
80}
81
82impl PropertyFunctionBinding {
83    /// Create a new empty binding
84    pub fn new() -> Self {
85        Self {
86            bindings: HashMap::new(),
87        }
88    }
89
90    /// Add a variable binding
91    pub fn bind(mut self, var: impl Into<String>, term: Term) -> Self {
92        self.bindings.insert(var.into(), term);
93        self
94    }
95
96    /// Get a binding value
97    pub fn get(&self, var: &str) -> Option<&Term> {
98        self.bindings.get(var)
99    }
100
101    /// Get all bindings
102    pub fn bindings(&self) -> &HashMap<String, Term> {
103        &self.bindings
104    }
105
106    /// Number of bindings
107    pub fn len(&self) -> usize {
108        self.bindings.len()
109    }
110
111    /// Whether there are no bindings
112    pub fn is_empty(&self) -> bool {
113        self.bindings.is_empty()
114    }
115}
116
117impl Default for PropertyFunctionBinding {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123/// The result of evaluating a property function: zero or more binding rows.
124#[derive(Debug, Clone)]
125pub struct PropertyFunctionResult {
126    /// Rows of bindings
127    rows: Vec<PropertyFunctionBinding>,
128}
129
130impl PropertyFunctionResult {
131    /// Create result with no rows (empty result set)
132    pub fn empty() -> Self {
133        Self { rows: Vec::new() }
134    }
135
136    /// Create result with given rows
137    pub fn from_rows(rows: Vec<PropertyFunctionBinding>) -> Self {
138        Self { rows }
139    }
140
141    /// Create result with a single row
142    pub fn single(binding: PropertyFunctionBinding) -> Self {
143        Self {
144            rows: vec![binding],
145        }
146    }
147
148    /// Get all result rows
149    pub fn rows(&self) -> &[PropertyFunctionBinding] {
150        &self.rows
151    }
152
153    /// Number of result rows
154    pub fn len(&self) -> usize {
155        self.rows.len()
156    }
157
158    /// Whether there are no result rows
159    pub fn is_empty(&self) -> bool {
160        self.rows.is_empty()
161    }
162
163    /// Iterate over result rows
164    pub fn iter(&self) -> impl Iterator<Item = &PropertyFunctionBinding> {
165        self.rows.iter()
166    }
167}
168
169/// Metadata describing a property function.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct PropertyFunctionMetadata {
172    /// The IRI of the property function
173    pub iri: String,
174    /// Human-readable name
175    pub name: String,
176    /// Description of what the function does
177    pub description: String,
178    /// Whether the subject must be bound
179    pub subject_must_be_bound: bool,
180    /// Whether the object must be bound
181    pub object_must_be_bound: bool,
182    /// Minimum number of subject arguments
183    pub min_subject_args: usize,
184    /// Maximum number of subject arguments (None = unlimited)
185    pub max_subject_args: Option<usize>,
186    /// Minimum number of object arguments
187    pub min_object_args: usize,
188    /// Maximum number of object arguments (None = unlimited)
189    pub max_object_args: Option<usize>,
190    /// Category (e.g., "list", "text", "spatial")
191    pub category: String,
192}
193
194/// Core trait for property function implementations.
195///
196/// A property function is invoked when the SPARQL engine encounters
197/// a triple pattern whose predicate matches a registered IRI.
198pub trait PropertyFunction: Send + Sync + fmt::Debug {
199    /// Return metadata describing this property function.
200    fn metadata(&self) -> PropertyFunctionMetadata;
201
202    /// Evaluate the property function given the subject and object arguments.
203    ///
204    /// # Arguments
205    /// * `subject` - The subject side of the triple pattern
206    /// * `object` - The object side of the triple pattern
207    ///
208    /// # Returns
209    /// A set of bindings produced by the evaluation.
210    fn evaluate(
211        &self,
212        subject: &PropertyFunctionArg,
213        object: &PropertyFunctionArg,
214    ) -> Result<PropertyFunctionResult, OxirsError>;
215
216    /// Validate arguments before evaluation (optional override).
217    fn validate(
218        &self,
219        subject: &PropertyFunctionArg,
220        object: &PropertyFunctionArg,
221    ) -> Result<(), OxirsError> {
222        let meta = self.metadata();
223
224        // Validate subject argument count for lists
225        if let PropertyFunctionArg::List(args) = subject {
226            if args.len() < meta.min_subject_args {
227                return Err(OxirsError::Query(format!(
228                    "Property function {} requires at least {} subject arguments, got {}",
229                    meta.iri,
230                    meta.min_subject_args,
231                    args.len()
232                )));
233            }
234            if let Some(max) = meta.max_subject_args {
235                if args.len() > max {
236                    return Err(OxirsError::Query(format!(
237                        "Property function {} accepts at most {} subject arguments, got {}",
238                        meta.iri,
239                        max,
240                        args.len()
241                    )));
242                }
243            }
244        }
245
246        // Validate object argument count for lists
247        if let PropertyFunctionArg::List(args) = object {
248            if args.len() < meta.min_object_args {
249                return Err(OxirsError::Query(format!(
250                    "Property function {} requires at least {} object arguments, got {}",
251                    meta.iri,
252                    meta.min_object_args,
253                    args.len()
254                )));
255            }
256            if let Some(max) = meta.max_object_args {
257                if args.len() > max {
258                    return Err(OxirsError::Query(format!(
259                        "Property function {} accepts at most {} object arguments, got {}",
260                        meta.iri,
261                        max,
262                        args.len()
263                    )));
264                }
265            }
266        }
267
268        Ok(())
269    }
270
271    /// Estimated cardinality of results (for query planning).
272    /// Returns None if unknown. Default is None.
273    fn estimated_cardinality(
274        &self,
275        _subject: &PropertyFunctionArg,
276        _object: &PropertyFunctionArg,
277    ) -> Option<u64> {
278        None
279    }
280}
281
282/// Factory for creating property function instances (Jena-compatible pattern).
283pub trait PropertyFunctionFactory: Send + Sync {
284    /// Create a new property function instance for the given IRI.
285    fn create(&self, iri: &str) -> Result<Box<dyn PropertyFunction>, OxirsError>;
286}
287
288/// Registry of property functions, keyed by predicate IRI.
289pub struct PropertyFunctionRegistry {
290    /// Registered property function instances
291    functions: HashMap<String, Arc<dyn PropertyFunction>>,
292    /// Registered factories (lazy creation)
293    factories: HashMap<String, Arc<dyn PropertyFunctionFactory>>,
294    /// Execution statistics
295    stats: PropertyFunctionStats,
296}
297
298/// Execution statistics for property functions.
299#[derive(Debug, Clone, Default)]
300pub struct PropertyFunctionStats {
301    /// Total evaluations
302    pub total_evaluations: u64,
303    /// Total rows produced
304    pub total_rows_produced: u64,
305    /// Total errors
306    pub total_errors: u64,
307    /// Per-function evaluation counts
308    pub per_function_counts: HashMap<String, u64>,
309}
310
311impl Default for PropertyFunctionRegistry {
312    fn default() -> Self {
313        let mut registry = Self {
314            functions: HashMap::new(),
315            factories: HashMap::new(),
316            stats: PropertyFunctionStats::default(),
317        };
318        registry.register_builtins();
319        registry
320    }
321}
322
323impl PropertyFunctionRegistry {
324    /// Create a new registry with built-in property functions.
325    pub fn new() -> Self {
326        Self::default()
327    }
328
329    /// Create an empty registry without built-ins (for testing).
330    pub fn empty() -> Self {
331        Self {
332            functions: HashMap::new(),
333            factories: HashMap::new(),
334            stats: PropertyFunctionStats::default(),
335        }
336    }
337
338    /// Register a property function instance for a given IRI.
339    pub fn register(&mut self, iri: impl Into<String>, func: Arc<dyn PropertyFunction>) {
340        self.functions.insert(iri.into(), func);
341    }
342
343    /// Register a factory for lazy creation of property functions.
344    pub fn register_factory(
345        &mut self,
346        iri: impl Into<String>,
347        factory: Arc<dyn PropertyFunctionFactory>,
348    ) {
349        self.factories.insert(iri.into(), factory);
350    }
351
352    /// Unregister a property function.
353    pub fn unregister(&mut self, iri: &str) -> bool {
354        let removed_func = self.functions.remove(iri).is_some();
355        let removed_factory = self.factories.remove(iri).is_some();
356        removed_func || removed_factory
357    }
358
359    /// Check if an IRI is registered as a property function.
360    pub fn is_property_function(&self, iri: &str) -> bool {
361        self.functions.contains_key(iri) || self.factories.contains_key(iri)
362    }
363
364    /// Look up a property function by IRI.
365    pub fn lookup(&mut self, iri: &str) -> Option<Arc<dyn PropertyFunction>> {
366        // First, check direct registrations
367        if let Some(func) = self.functions.get(iri) {
368            return Some(Arc::clone(func));
369        }
370
371        // Try factories (lazy creation)
372        if let Some(factory) = self.factories.get(iri) {
373            match factory.create(iri) {
374                Ok(func) => {
375                    let func: Arc<dyn PropertyFunction> = Arc::from(func);
376                    self.functions.insert(iri.to_string(), Arc::clone(&func));
377                    return Some(func);
378                }
379                Err(_) => return None,
380            }
381        }
382
383        None
384    }
385
386    /// Evaluate a property function by IRI.
387    pub fn evaluate(
388        &mut self,
389        iri: &str,
390        subject: &PropertyFunctionArg,
391        object: &PropertyFunctionArg,
392    ) -> Result<PropertyFunctionResult, OxirsError> {
393        let func = self
394            .lookup(iri)
395            .ok_or_else(|| OxirsError::Query(format!("Unknown property function: {iri}")))?;
396
397        // Validate first
398        func.validate(subject, object)?;
399
400        // Evaluate
401        let result = func.evaluate(subject, object);
402
403        // Update statistics
404        self.stats.total_evaluations += 1;
405        *self
406            .stats
407            .per_function_counts
408            .entry(iri.to_string())
409            .or_insert(0) += 1;
410
411        match &result {
412            Ok(r) => {
413                self.stats.total_rows_produced += r.len() as u64;
414            }
415            Err(_) => {
416                self.stats.total_errors += 1;
417            }
418        }
419
420        result
421    }
422
423    /// Get all registered IRIs.
424    pub fn registered_iris(&self) -> Vec<String> {
425        let mut iris: Vec<String> = self.functions.keys().cloned().collect();
426        for iri in self.factories.keys() {
427            if !iris.contains(iri) {
428                iris.push(iri.clone());
429            }
430        }
431        iris.sort();
432        iris
433    }
434
435    /// Get metadata for all registered property functions.
436    pub fn all_metadata(&self) -> Vec<PropertyFunctionMetadata> {
437        self.functions.values().map(|f| f.metadata()).collect()
438    }
439
440    /// Get execution statistics.
441    pub fn statistics(&self) -> &PropertyFunctionStats {
442        &self.stats
443    }
444
445    /// Reset execution statistics.
446    pub fn reset_statistics(&mut self) {
447        self.stats = PropertyFunctionStats::default();
448    }
449
450    /// Number of registered property functions.
451    pub fn len(&self) -> usize {
452        let mut count = self.functions.len();
453        for iri in self.factories.keys() {
454            if !self.functions.contains_key(iri) {
455                count += 1;
456            }
457        }
458        count
459    }
460
461    /// Whether the registry is empty.
462    pub fn is_empty(&self) -> bool {
463        self.functions.is_empty() && self.factories.is_empty()
464    }
465
466    /// Register all built-in property functions.
467    fn register_builtins(&mut self) {
468        // List operations
469        self.register(
470            "http://jena.apache.org/ARQ/list#member",
471            Arc::new(ListMemberPF::new()),
472        );
473        self.register(
474            "http://jena.apache.org/ARQ/list#index",
475            Arc::new(ListIndexPF::new()),
476        );
477        self.register(
478            "http://jena.apache.org/ARQ/list#length",
479            Arc::new(ListLengthPF::new()),
480        );
481
482        // String decomposition
483        self.register(
484            "http://jena.apache.org/ARQ/property#splitIRI",
485            Arc::new(SplitIriPF::new()),
486        );
487        self.register(
488            "http://jena.apache.org/ARQ/property#localname",
489            Arc::new(LocalNamePF::new()),
490        );
491        self.register(
492            "http://jena.apache.org/ARQ/property#namespace",
493            Arc::new(NamespacePF::new()),
494        );
495
496        // Text search (lightweight built-in)
497        self.register(
498            "http://jena.apache.org/text#search",
499            Arc::new(TextSearchPF::new()),
500        );
501
502        // String concat property function
503        self.register(
504            "http://jena.apache.org/ARQ/property#concat",
505            Arc::new(ConcatPF::new()),
506        );
507
508        // Str split property function
509        self.register(
510            "http://jena.apache.org/ARQ/property#strSplit",
511            Arc::new(StrSplitPF::new()),
512        );
513    }
514}
515
516impl fmt::Debug for PropertyFunctionRegistry {
517    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518        f.debug_struct("PropertyFunctionRegistry")
519            .field("registered_count", &self.len())
520            .field("stats", &self.stats)
521            .finish()
522    }
523}
524
525// =============================================================================
526// Built-in property function implementations
527// =============================================================================
528
529/// list:member - Enumerate members of an RDF list.
530///
531/// Usage: `?list list:member ?item`
532///   Subject: a variable or term representing the list head
533///   Object: a variable to bind each member to
534#[derive(Debug)]
535pub struct ListMemberPF {
536    _private: (),
537}
538
539impl ListMemberPF {
540    pub fn new() -> Self {
541        Self { _private: () }
542    }
543}
544
545impl Default for ListMemberPF {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551impl PropertyFunction for ListMemberPF {
552    fn metadata(&self) -> PropertyFunctionMetadata {
553        PropertyFunctionMetadata {
554            iri: "http://jena.apache.org/ARQ/list#member".to_string(),
555            name: "list:member".to_string(),
556            description: "Enumerates all members of an RDF list".to_string(),
557            subject_must_be_bound: false,
558            object_must_be_bound: false,
559            min_subject_args: 0,
560            max_subject_args: None,
561            min_object_args: 0,
562            max_object_args: None,
563            category: "list".to_string(),
564        }
565    }
566
567    fn evaluate(
568        &self,
569        subject: &PropertyFunctionArg,
570        _object: &PropertyFunctionArg,
571    ) -> Result<PropertyFunctionResult, OxirsError> {
572        // If subject is a list, enumerate its members
573        match subject {
574            PropertyFunctionArg::List(members) => {
575                let mut rows = Vec::new();
576                for (i, member) in members.iter().enumerate() {
577                    if let PropertyFunctionArg::Term(term) = member {
578                        let binding = PropertyFunctionBinding::new()
579                            .bind("index", make_integer_term(i as i64))
580                            .bind("member", term.clone());
581                        rows.push(binding);
582                    }
583                }
584                Ok(PropertyFunctionResult::from_rows(rows))
585            }
586            PropertyFunctionArg::Term(term) => {
587                // Single term: return it as the only member
588                let binding = PropertyFunctionBinding::new()
589                    .bind("index", make_integer_term(0))
590                    .bind("member", term.clone());
591                Ok(PropertyFunctionResult::single(binding))
592            }
593            PropertyFunctionArg::Variable(_) => {
594                // Cannot enumerate without a bound list
595                Ok(PropertyFunctionResult::empty())
596            }
597        }
598    }
599
600    fn estimated_cardinality(
601        &self,
602        subject: &PropertyFunctionArg,
603        _object: &PropertyFunctionArg,
604    ) -> Option<u64> {
605        match subject {
606            PropertyFunctionArg::List(members) => Some(members.len() as u64),
607            PropertyFunctionArg::Term(_) => Some(1),
608            PropertyFunctionArg::Variable(_) => None,
609        }
610    }
611}
612
613/// list:index - Return member at a specific index in an RDF list.
614///
615/// Usage: `?list list:index (?index ?item)`
616#[derive(Debug)]
617pub struct ListIndexPF {
618    _private: (),
619}
620
621impl ListIndexPF {
622    pub fn new() -> Self {
623        Self { _private: () }
624    }
625}
626
627impl Default for ListIndexPF {
628    fn default() -> Self {
629        Self::new()
630    }
631}
632
633impl PropertyFunction for ListIndexPF {
634    fn metadata(&self) -> PropertyFunctionMetadata {
635        PropertyFunctionMetadata {
636            iri: "http://jena.apache.org/ARQ/list#index".to_string(),
637            name: "list:index".to_string(),
638            description: "Returns (index, member) pairs for an RDF list".to_string(),
639            subject_must_be_bound: false,
640            object_must_be_bound: false,
641            min_subject_args: 0,
642            max_subject_args: None,
643            min_object_args: 0,
644            max_object_args: Some(2),
645            category: "list".to_string(),
646        }
647    }
648
649    fn evaluate(
650        &self,
651        subject: &PropertyFunctionArg,
652        _object: &PropertyFunctionArg,
653    ) -> Result<PropertyFunctionResult, OxirsError> {
654        match subject {
655            PropertyFunctionArg::List(members) => {
656                let mut rows = Vec::new();
657                for (i, member) in members.iter().enumerate() {
658                    if let PropertyFunctionArg::Term(term) = member {
659                        let binding = PropertyFunctionBinding::new()
660                            .bind("index", make_integer_term(i as i64))
661                            .bind("item", term.clone());
662                        rows.push(binding);
663                    }
664                }
665                Ok(PropertyFunctionResult::from_rows(rows))
666            }
667            _ => Ok(PropertyFunctionResult::empty()),
668        }
669    }
670}
671
672/// list:length - Return the length of an RDF list.
673///
674/// Usage: `?list list:length ?len`
675#[derive(Debug)]
676pub struct ListLengthPF {
677    _private: (),
678}
679
680impl ListLengthPF {
681    pub fn new() -> Self {
682        Self { _private: () }
683    }
684}
685
686impl Default for ListLengthPF {
687    fn default() -> Self {
688        Self::new()
689    }
690}
691
692impl PropertyFunction for ListLengthPF {
693    fn metadata(&self) -> PropertyFunctionMetadata {
694        PropertyFunctionMetadata {
695            iri: "http://jena.apache.org/ARQ/list#length".to_string(),
696            name: "list:length".to_string(),
697            description: "Returns the length of an RDF list".to_string(),
698            subject_must_be_bound: true,
699            object_must_be_bound: false,
700            min_subject_args: 0,
701            max_subject_args: None,
702            min_object_args: 0,
703            max_object_args: Some(1),
704            category: "list".to_string(),
705        }
706    }
707
708    fn evaluate(
709        &self,
710        subject: &PropertyFunctionArg,
711        _object: &PropertyFunctionArg,
712    ) -> Result<PropertyFunctionResult, OxirsError> {
713        let len = match subject {
714            PropertyFunctionArg::List(members) => members.len(),
715            PropertyFunctionArg::Term(_) => 1,
716            PropertyFunctionArg::Variable(_) => {
717                return Err(OxirsError::Query(
718                    "list:length requires a bound subject".to_string(),
719                ));
720            }
721        };
722
723        let binding = PropertyFunctionBinding::new().bind("length", make_integer_term(len as i64));
724        Ok(PropertyFunctionResult::single(binding))
725    }
726
727    fn estimated_cardinality(
728        &self,
729        _subject: &PropertyFunctionArg,
730        _object: &PropertyFunctionArg,
731    ) -> Option<u64> {
732        Some(1) // Always returns exactly one row
733    }
734}
735
736/// apf:splitIRI - Decompose an IRI into namespace and local name.
737///
738/// Usage: `?iri apf:splitIRI (?namespace ?localname)`
739#[derive(Debug)]
740pub struct SplitIriPF {
741    _private: (),
742}
743
744impl SplitIriPF {
745    pub fn new() -> Self {
746        Self { _private: () }
747    }
748}
749
750impl Default for SplitIriPF {
751    fn default() -> Self {
752        Self::new()
753    }
754}
755
756impl PropertyFunction for SplitIriPF {
757    fn metadata(&self) -> PropertyFunctionMetadata {
758        PropertyFunctionMetadata {
759            iri: "http://jena.apache.org/ARQ/property#splitIRI".to_string(),
760            name: "apf:splitIRI".to_string(),
761            description: "Splits an IRI into namespace and local name".to_string(),
762            subject_must_be_bound: true,
763            object_must_be_bound: false,
764            min_subject_args: 1,
765            max_subject_args: Some(1),
766            min_object_args: 0,
767            max_object_args: Some(2),
768            category: "string".to_string(),
769        }
770    }
771
772    fn evaluate(
773        &self,
774        subject: &PropertyFunctionArg,
775        _object: &PropertyFunctionArg,
776    ) -> Result<PropertyFunctionResult, OxirsError> {
777        let iri_str = match subject {
778            PropertyFunctionArg::Term(term) => extract_iri_string(term)?,
779            PropertyFunctionArg::Variable(_) => {
780                return Err(OxirsError::Query(
781                    "apf:splitIRI requires a bound IRI subject".to_string(),
782                ));
783            }
784            PropertyFunctionArg::List(args) => {
785                if let Some(PropertyFunctionArg::Term(term)) = args.first() {
786                    extract_iri_string(term)?
787                } else {
788                    return Err(OxirsError::Query(
789                        "apf:splitIRI requires an IRI argument".to_string(),
790                    ));
791                }
792            }
793        };
794
795        // Split at the last # or /
796        let (namespace, local_name) = split_iri(&iri_str);
797
798        let binding = PropertyFunctionBinding::new()
799            .bind("namespace", make_string_term(&namespace))
800            .bind("localname", make_string_term(&local_name));
801
802        Ok(PropertyFunctionResult::single(binding))
803    }
804
805    fn estimated_cardinality(
806        &self,
807        _subject: &PropertyFunctionArg,
808        _object: &PropertyFunctionArg,
809    ) -> Option<u64> {
810        Some(1)
811    }
812}
813
814/// apf:localname - Extract the local name from an IRI.
815#[derive(Debug)]
816pub struct LocalNamePF {
817    _private: (),
818}
819
820impl LocalNamePF {
821    pub fn new() -> Self {
822        Self { _private: () }
823    }
824}
825
826impl Default for LocalNamePF {
827    fn default() -> Self {
828        Self::new()
829    }
830}
831
832impl PropertyFunction for LocalNamePF {
833    fn metadata(&self) -> PropertyFunctionMetadata {
834        PropertyFunctionMetadata {
835            iri: "http://jena.apache.org/ARQ/property#localname".to_string(),
836            name: "apf:localname".to_string(),
837            description: "Extracts the local name from an IRI".to_string(),
838            subject_must_be_bound: true,
839            object_must_be_bound: false,
840            min_subject_args: 1,
841            max_subject_args: Some(1),
842            min_object_args: 0,
843            max_object_args: Some(1),
844            category: "string".to_string(),
845        }
846    }
847
848    fn evaluate(
849        &self,
850        subject: &PropertyFunctionArg,
851        _object: &PropertyFunctionArg,
852    ) -> Result<PropertyFunctionResult, OxirsError> {
853        let iri_str = extract_arg_iri(subject)?;
854        let (_, local_name) = split_iri(&iri_str);
855
856        let binding =
857            PropertyFunctionBinding::new().bind("localname", make_string_term(&local_name));
858        Ok(PropertyFunctionResult::single(binding))
859    }
860
861    fn estimated_cardinality(
862        &self,
863        _subject: &PropertyFunctionArg,
864        _object: &PropertyFunctionArg,
865    ) -> Option<u64> {
866        Some(1)
867    }
868}
869
870/// apf:namespace - Extract the namespace from an IRI.
871#[derive(Debug)]
872pub struct NamespacePF {
873    _private: (),
874}
875
876impl NamespacePF {
877    pub fn new() -> Self {
878        Self { _private: () }
879    }
880}
881
882impl Default for NamespacePF {
883    fn default() -> Self {
884        Self::new()
885    }
886}
887
888impl PropertyFunction for NamespacePF {
889    fn metadata(&self) -> PropertyFunctionMetadata {
890        PropertyFunctionMetadata {
891            iri: "http://jena.apache.org/ARQ/property#namespace".to_string(),
892            name: "apf:namespace".to_string(),
893            description: "Extracts the namespace from an IRI".to_string(),
894            subject_must_be_bound: true,
895            object_must_be_bound: false,
896            min_subject_args: 1,
897            max_subject_args: Some(1),
898            min_object_args: 0,
899            max_object_args: Some(1),
900            category: "string".to_string(),
901        }
902    }
903
904    fn evaluate(
905        &self,
906        subject: &PropertyFunctionArg,
907        _object: &PropertyFunctionArg,
908    ) -> Result<PropertyFunctionResult, OxirsError> {
909        let iri_str = extract_arg_iri(subject)?;
910        let (namespace, _) = split_iri(&iri_str);
911
912        let binding =
913            PropertyFunctionBinding::new().bind("namespace", make_string_term(&namespace));
914        Ok(PropertyFunctionResult::single(binding))
915    }
916}
917
918/// text:search - Simple text search property function.
919///
920/// Usage: `?s text:search ("search terms" ?score)`
921#[derive(Debug)]
922pub struct TextSearchPF {
923    _private: (),
924}
925
926impl TextSearchPF {
927    pub fn new() -> Self {
928        Self { _private: () }
929    }
930}
931
932impl Default for TextSearchPF {
933    fn default() -> Self {
934        Self::new()
935    }
936}
937
938impl PropertyFunction for TextSearchPF {
939    fn metadata(&self) -> PropertyFunctionMetadata {
940        PropertyFunctionMetadata {
941            iri: "http://jena.apache.org/text#search".to_string(),
942            name: "text:search".to_string(),
943            description: "Full-text search across literal values".to_string(),
944            subject_must_be_bound: false,
945            object_must_be_bound: true,
946            min_subject_args: 0,
947            max_subject_args: None,
948            min_object_args: 1,
949            max_object_args: Some(3),
950            category: "text".to_string(),
951        }
952    }
953
954    fn evaluate(
955        &self,
956        _subject: &PropertyFunctionArg,
957        object: &PropertyFunctionArg,
958    ) -> Result<PropertyFunctionResult, OxirsError> {
959        // Extract search query from object
960        let query = match object {
961            PropertyFunctionArg::Term(term) => extract_string_value(term),
962            PropertyFunctionArg::List(args) => {
963                if let Some(PropertyFunctionArg::Term(term)) = args.first() {
964                    extract_string_value(term)
965                } else {
966                    return Err(OxirsError::Query(
967                        "text:search requires a search query string".to_string(),
968                    ));
969                }
970            }
971            PropertyFunctionArg::Variable(_) => {
972                return Err(OxirsError::Query(
973                    "text:search requires a bound search query".to_string(),
974                ));
975            }
976        };
977
978        // In a full implementation, this would query a text index.
979        // For now, return a placeholder binding showing the query was parsed.
980        let binding = PropertyFunctionBinding::new()
981            .bind("query", make_string_term(&query))
982            .bind("score", make_double_term(1.0));
983
984        Ok(PropertyFunctionResult::single(binding))
985    }
986}
987
988/// apf:concat - Concatenate multiple string arguments.
989///
990/// Usage: `(?a ?b ?c) apf:concat ?result`
991#[derive(Debug)]
992pub struct ConcatPF {
993    _private: (),
994}
995
996impl ConcatPF {
997    pub fn new() -> Self {
998        Self { _private: () }
999    }
1000}
1001
1002impl Default for ConcatPF {
1003    fn default() -> Self {
1004        Self::new()
1005    }
1006}
1007
1008impl PropertyFunction for ConcatPF {
1009    fn metadata(&self) -> PropertyFunctionMetadata {
1010        PropertyFunctionMetadata {
1011            iri: "http://jena.apache.org/ARQ/property#concat".to_string(),
1012            name: "apf:concat".to_string(),
1013            description: "Concatenates string arguments into a single string".to_string(),
1014            subject_must_be_bound: true,
1015            object_must_be_bound: false,
1016            min_subject_args: 1,
1017            max_subject_args: None,
1018            min_object_args: 0,
1019            max_object_args: Some(1),
1020            category: "string".to_string(),
1021        }
1022    }
1023
1024    fn evaluate(
1025        &self,
1026        subject: &PropertyFunctionArg,
1027        _object: &PropertyFunctionArg,
1028    ) -> Result<PropertyFunctionResult, OxirsError> {
1029        let parts: Vec<String> = match subject {
1030            PropertyFunctionArg::List(args) => args
1031                .iter()
1032                .filter_map(|a| {
1033                    if let PropertyFunctionArg::Term(t) = a {
1034                        Some(extract_string_value(t))
1035                    } else {
1036                        None
1037                    }
1038                })
1039                .collect(),
1040            PropertyFunctionArg::Term(term) => vec![extract_string_value(term)],
1041            PropertyFunctionArg::Variable(_) => {
1042                return Err(OxirsError::Query(
1043                    "apf:concat requires bound string arguments".to_string(),
1044                ));
1045            }
1046        };
1047
1048        let concatenated = parts.join("");
1049        let binding =
1050            PropertyFunctionBinding::new().bind("result", make_string_term(&concatenated));
1051        Ok(PropertyFunctionResult::single(binding))
1052    }
1053
1054    fn estimated_cardinality(
1055        &self,
1056        _subject: &PropertyFunctionArg,
1057        _object: &PropertyFunctionArg,
1058    ) -> Option<u64> {
1059        Some(1)
1060    }
1061}
1062
1063/// apf:strSplit - Split a string by a delimiter.
1064///
1065/// Usage: `?str apf:strSplit (?delimiter ?part)`
1066#[derive(Debug)]
1067pub struct StrSplitPF {
1068    _private: (),
1069}
1070
1071impl StrSplitPF {
1072    pub fn new() -> Self {
1073        Self { _private: () }
1074    }
1075}
1076
1077impl Default for StrSplitPF {
1078    fn default() -> Self {
1079        Self::new()
1080    }
1081}
1082
1083impl PropertyFunction for StrSplitPF {
1084    fn metadata(&self) -> PropertyFunctionMetadata {
1085        PropertyFunctionMetadata {
1086            iri: "http://jena.apache.org/ARQ/property#strSplit".to_string(),
1087            name: "apf:strSplit".to_string(),
1088            description: "Splits a string by a delimiter, producing multiple bindings".to_string(),
1089            subject_must_be_bound: true,
1090            object_must_be_bound: false,
1091            min_subject_args: 1,
1092            max_subject_args: Some(1),
1093            min_object_args: 1,
1094            max_object_args: Some(2),
1095            category: "string".to_string(),
1096        }
1097    }
1098
1099    fn evaluate(
1100        &self,
1101        subject: &PropertyFunctionArg,
1102        object: &PropertyFunctionArg,
1103    ) -> Result<PropertyFunctionResult, OxirsError> {
1104        let input = extract_arg_string(subject)?;
1105
1106        // Get delimiter from object
1107        let delimiter = match object {
1108            PropertyFunctionArg::Term(term) => extract_string_value(term),
1109            PropertyFunctionArg::List(args) => {
1110                if let Some(PropertyFunctionArg::Term(term)) = args.first() {
1111                    extract_string_value(term)
1112                } else {
1113                    ",".to_string() // Default delimiter
1114                }
1115            }
1116            PropertyFunctionArg::Variable(_) => ",".to_string(),
1117        };
1118
1119        let parts: Vec<&str> = input.split(&delimiter).collect();
1120        let mut rows = Vec::new();
1121        for (i, part) in parts.iter().enumerate() {
1122            let binding = PropertyFunctionBinding::new()
1123                .bind("index", make_integer_term(i as i64))
1124                .bind("part", make_string_term(part));
1125            rows.push(binding);
1126        }
1127
1128        Ok(PropertyFunctionResult::from_rows(rows))
1129    }
1130}
1131
1132// =============================================================================
1133// Helper functions
1134// =============================================================================
1135
1136/// Create an xsd:integer Term.
1137fn make_integer_term(value: i64) -> Term {
1138    Term::Literal(crate::model::Literal::new_typed(
1139        value.to_string(),
1140        crate::model::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer"),
1141    ))
1142}
1143
1144/// Create an xsd:double Term.
1145fn make_double_term(value: f64) -> Term {
1146    Term::Literal(crate::model::Literal::new_typed(
1147        value.to_string(),
1148        crate::model::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#double"),
1149    ))
1150}
1151
1152/// Create an xsd:string Term.
1153fn make_string_term(value: &str) -> Term {
1154    Term::Literal(crate::model::Literal::new(value))
1155}
1156
1157/// Extract the string representation from a Term.
1158fn extract_string_value(term: &Term) -> String {
1159    match term {
1160        Term::Literal(lit) => lit.value().to_string(),
1161        Term::NamedNode(nn) => nn.as_str().to_string(),
1162        Term::BlankNode(bn) => bn.as_str().to_string(),
1163        _ => format!("{term:?}"),
1164    }
1165}
1166
1167/// Extract IRI string from a Term.
1168fn extract_iri_string(term: &Term) -> Result<String, OxirsError> {
1169    match term {
1170        Term::NamedNode(nn) => Ok(nn.as_str().to_string()),
1171        _ => Err(OxirsError::Query(format!("Expected IRI, got: {term:?}"))),
1172    }
1173}
1174
1175/// Extract IRI from a PropertyFunctionArg.
1176fn extract_arg_iri(arg: &PropertyFunctionArg) -> Result<String, OxirsError> {
1177    match arg {
1178        PropertyFunctionArg::Term(term) => extract_iri_string(term),
1179        PropertyFunctionArg::List(args) => {
1180            if let Some(PropertyFunctionArg::Term(term)) = args.first() {
1181                extract_iri_string(term)
1182            } else {
1183                Err(OxirsError::Query(
1184                    "Expected IRI argument in list".to_string(),
1185                ))
1186            }
1187        }
1188        PropertyFunctionArg::Variable(v) => Err(OxirsError::Query(format!(
1189            "Expected bound IRI, got unbound variable ?{v}"
1190        ))),
1191    }
1192}
1193
1194/// Extract string from a PropertyFunctionArg.
1195fn extract_arg_string(arg: &PropertyFunctionArg) -> Result<String, OxirsError> {
1196    match arg {
1197        PropertyFunctionArg::Term(term) => Ok(extract_string_value(term)),
1198        PropertyFunctionArg::List(args) => {
1199            if let Some(PropertyFunctionArg::Term(term)) = args.first() {
1200                Ok(extract_string_value(term))
1201            } else {
1202                Err(OxirsError::Query(
1203                    "Expected string argument in list".to_string(),
1204                ))
1205            }
1206        }
1207        PropertyFunctionArg::Variable(v) => Err(OxirsError::Query(format!(
1208            "Expected bound string, got unbound variable ?{v}"
1209        ))),
1210    }
1211}
1212
1213/// Split an IRI into namespace and local name at the last `#`, `/`, or `:`.
1214fn split_iri(iri: &str) -> (String, String) {
1215    // Try # first, then /, then : (for URN-style IRIs like urn:isbn:123456)
1216    if let Some(pos) = iri.rfind('#') {
1217        (iri[..=pos].to_string(), iri[pos + 1..].to_string())
1218    } else if let Some(pos) = iri.rfind('/') {
1219        (iri[..=pos].to_string(), iri[pos + 1..].to_string())
1220    } else if let Some(pos) = iri.rfind(':') {
1221        (iri[..=pos].to_string(), iri[pos + 1..].to_string())
1222    } else {
1223        (String::new(), iri.to_string())
1224    }
1225}
1226
1227#[cfg(test)]
1228#[path = "property_function_registry_tests.rs"]
1229mod tests;