Skip to main content

oxirs_core/sparql/
patterns.rs

1//! SPARQL pattern matching: OPTIONAL and UNION clauses
2
3use crate::model::{Quad, Term};
4use crate::rdf_store::VariableBinding;
5use crate::Result;
6
7/// Simple triple pattern for matching
8#[derive(Debug, Clone)]
9pub struct SimpleTriplePattern {
10    pub subject: Option<String>,
11    pub predicate: Option<String>,
12    pub object: Option<String>,
13}
14
15/// Pattern group (required or optional)
16#[derive(Debug, Clone)]
17pub struct PatternGroup {
18    pub patterns: Vec<SimpleTriplePattern>,
19    pub optional: bool,
20}
21
22/// Union group for SPARQL UNION clause
23#[derive(Debug, Clone)]
24pub struct UnionGroup {
25    pub branches: Vec<Vec<PatternGroup>>,
26}
27
28/// Check if query contains UNION
29pub fn has_union(sparql: &str) -> bool {
30    let sparql_upper = sparql.to_uppercase();
31    sparql_upper.contains(" UNION ")
32        || sparql_upper.contains("\nUNION\n")
33        || sparql_upper.contains("{UNION")
34}
35
36/// Find matching closing brace
37pub fn find_matching_brace(text: &str, start_pos: usize) -> Option<usize> {
38    let chars: Vec<char> = text.chars().collect();
39    if start_pos >= chars.len() || chars[start_pos] != '{' {
40        return None;
41    }
42
43    let mut brace_count = 1;
44    for (i, &ch) in chars.iter().enumerate().skip(start_pos + 1) {
45        if ch == '{' {
46            brace_count += 1;
47        } else if ch == '}' {
48            brace_count -= 1;
49            if brace_count == 0 {
50                return Some(i);
51            }
52        }
53    }
54
55    None
56}
57
58/// Parse a simple triple pattern from text
59pub fn parse_simple_pattern(text: &str) -> Option<SimpleTriplePattern> {
60    // Simple pattern: ?s ?p ?o . or <uri> <uri> "literal" .
61    let text = text.trim();
62
63    // Split by periods and process each potential pattern
64    for line in text.split('.') {
65        let line = line.trim();
66        if line.is_empty() {
67            continue;
68        }
69
70        // Skip FILTER, BIND, VALUES, UNION keywords
71        let line_upper = line.to_uppercase();
72        if line_upper.contains("FILTER")
73            || line_upper.contains("BIND")
74            || line_upper.contains("VALUES")
75            || line_upper.contains("UNION")
76        {
77            continue;
78        }
79
80        let parts: Vec<&str> = line.split_whitespace().collect();
81        if parts.len() >= 3 {
82            return Some(SimpleTriplePattern {
83                subject: Some(parts[0].to_string()),
84                predicate: Some(parts[1].to_string()),
85                object: Some(parts[2..].join(" ")),
86            });
87        }
88    }
89
90    None
91}
92
93/// Extract pattern groups (required and optional) from the graph pattern.
94///
95/// The `WHERE` keyword is optional per SPARQL 1.1
96/// (`WhereClause ::= 'WHERE'? GroupGraphPattern`); the group is located whether
97/// or not the keyword is spelled.
98pub fn extract_pattern_groups(sparql: &str) -> Result<Vec<PatternGroup>> {
99    let mut groups = Vec::new();
100
101    if let Some((where_open, where_close)) = super::query_locator::locate_where_group(sparql) {
102        let pattern_text = &sparql[where_open + 1..where_close];
103
104        // Check for OPTIONAL blocks
105        let sparql_upper = pattern_text.to_uppercase();
106        if sparql_upper.contains("OPTIONAL") {
107            // Parse with OPTIONAL support
108            let mut pos = 0;
109            let mut required_patterns = Vec::new();
110
111            while pos < pattern_text.len() {
112                // Look for OPTIONAL keyword
113                if let Some(opt_pos) = pattern_text[pos..].to_uppercase().find("OPTIONAL") {
114                    let abs_pos = pos + opt_pos;
115
116                    // Add any required patterns before OPTIONAL
117                    let before_optional = &pattern_text[pos..abs_pos];
118                    if let Some(req_pattern) = parse_simple_pattern(before_optional) {
119                        required_patterns.push(req_pattern);
120                    }
121
122                    // Find OPTIONAL block
123                    let after_optional = &pattern_text[abs_pos + 8..];
124                    if let Some(opt_brace) = after_optional.find('{') {
125                        if let Some(opt_end) = find_matching_brace(after_optional, opt_brace) {
126                            let optional_content = &after_optional[opt_brace + 1..opt_end];
127
128                            // Parse optional patterns
129                            if let Some(opt_pattern) = parse_simple_pattern(optional_content) {
130                                groups.push(PatternGroup {
131                                    patterns: vec![opt_pattern],
132                                    optional: true,
133                                });
134                            }
135
136                            pos = abs_pos + 8 + opt_end + 1;
137                        } else {
138                            break;
139                        }
140                    } else {
141                        break;
142                    }
143                } else {
144                    // No more OPTIONAL, add remaining as required
145                    if let Some(req_pattern) = parse_simple_pattern(&pattern_text[pos..]) {
146                        required_patterns.push(req_pattern);
147                    }
148                    break;
149                }
150            }
151
152            // Add required patterns group
153            if !required_patterns.is_empty() {
154                groups.push(PatternGroup {
155                    patterns: required_patterns,
156                    optional: false,
157                });
158            }
159        } else {
160            // No OPTIONAL - all patterns are required
161            if let Some(pattern) = parse_simple_pattern(pattern_text) {
162                groups.push(PatternGroup {
163                    patterns: vec![pattern],
164                    optional: false,
165                });
166            }
167        }
168    }
169
170    Ok(groups)
171}
172
173/// Apply optional patterns to extend existing bindings
174pub fn apply_optional_patterns<F>(
175    bindings: Vec<VariableBinding>,
176    patterns: &[SimpleTriplePattern],
177    query_quads: F,
178) -> Result<Vec<VariableBinding>>
179where
180    F: Fn(&SimpleTriplePattern) -> Result<Vec<Quad>>,
181{
182    let mut new_results = Vec::new();
183
184    for binding in bindings {
185        let mut extended = false;
186
187        // Try to extend this binding with optional patterns
188        for pattern in patterns {
189            let matching_quads = query_quads(pattern)?;
190
191            for quad in matching_quads {
192                let mut new_binding = binding.clone();
193                let mut compatible = true;
194
195                // Check subject compatibility
196                if let Some(var) = &pattern.subject {
197                    if let Some(var_name) = var.strip_prefix('?') {
198                        if let Some(existing) = binding.get(var_name) {
199                            let new_term = Term::from(quad.subject().clone());
200                            if format!("{:?}", existing) != format!("{:?}", new_term) {
201                                compatible = false;
202                            }
203                        } else {
204                            new_binding
205                                .bind(var_name.to_string(), Term::from(quad.subject().clone()));
206                        }
207                    }
208                }
209
210                // Check predicate compatibility
211                if compatible {
212                    if let Some(var) = &pattern.predicate {
213                        if let Some(var_name) = var.strip_prefix('?') {
214                            if let Some(existing) = binding.get(var_name) {
215                                let new_term = Term::from(quad.predicate().clone());
216                                if format!("{:?}", existing) != format!("{:?}", new_term) {
217                                    compatible = false;
218                                }
219                            } else {
220                                new_binding.bind(
221                                    var_name.to_string(),
222                                    Term::from(quad.predicate().clone()),
223                                );
224                            }
225                        }
226                    }
227                }
228
229                // Check object compatibility
230                if compatible {
231                    if let Some(var) = &pattern.object {
232                        if let Some(var_name) = var.strip_prefix('?') {
233                            if let Some(existing) = binding.get(var_name) {
234                                let new_term = Term::from(quad.object().clone());
235                                if format!("{:?}", existing) != format!("{:?}", new_term) {
236                                    compatible = false;
237                                }
238                            } else {
239                                new_binding
240                                    .bind(var_name.to_string(), Term::from(quad.object().clone()));
241                            }
242                        }
243                    }
244                }
245
246                if compatible {
247                    new_results.push(new_binding);
248                    extended = true;
249                }
250            }
251        }
252
253        // If no optional pattern matched, keep original binding
254        if !extended {
255            new_results.push(binding);
256        }
257    }
258
259    Ok(new_results)
260}
261
262/// Extract UNION groups from WHERE clause
263pub fn extract_union_groups(sparql: &str) -> Result<Option<UnionGroup>> {
264    if !has_union(sparql) {
265        return Ok(None);
266    }
267
268    if let Some((where_open, where_close)) = super::query_locator::locate_where_group(sparql) {
269        let content = &sparql[where_open + 1..where_close];
270
271        // Split by UNION keyword
272        let mut branches = Vec::new();
273        let mut current_branch = String::new();
274
275        let mut pos = 0;
276        while pos < content.len() {
277            if let Some(union_pos) = content[pos..].to_uppercase().find(" UNION ") {
278                let abs_pos = pos + union_pos;
279                current_branch.push_str(&content[pos..abs_pos]);
280
281                // Parse the branch we accumulated
282                if let Some(branch) = parse_union_branch(&current_branch)? {
283                    branches.push(branch);
284                }
285
286                current_branch.clear();
287                pos = abs_pos + 7; // Skip " UNION "
288            } else {
289                // Last branch
290                current_branch.push_str(&content[pos..]);
291                break;
292            }
293        }
294
295        // Parse final branch
296        if !current_branch.trim().is_empty() {
297            if let Some(branch) = parse_union_branch(&current_branch)? {
298                branches.push(branch);
299            }
300        }
301
302        if !branches.is_empty() {
303            return Ok(Some(UnionGroup { branches }));
304        }
305    }
306
307    Ok(None)
308}
309
310/// Parse a single UNION branch
311pub fn parse_union_branch(branch_text: &str) -> Result<Option<Vec<PatternGroup>>> {
312    let branch_text = branch_text.trim();
313
314    // Branch can be either { pattern } or just pattern
315    let pattern_text = if branch_text.starts_with('{') && branch_text.ends_with('}') {
316        &branch_text[1..branch_text.len() - 1]
317    } else {
318        branch_text
319    };
320
321    let mut groups = Vec::new();
322
323    // Check for OPTIONAL in the branch
324    if pattern_text.to_uppercase().contains("OPTIONAL") {
325        // Parse with OPTIONAL support
326        let mut pos = 0;
327        let mut required_patterns = Vec::new();
328
329        while pos < pattern_text.len() {
330            if let Some(opt_pos) = pattern_text[pos..].to_uppercase().find("OPTIONAL") {
331                let abs_pos = pos + opt_pos;
332
333                // Add required patterns before OPTIONAL
334                let before_optional = &pattern_text[pos..abs_pos];
335                if let Some(req_pattern) = parse_simple_pattern(before_optional) {
336                    required_patterns.push(req_pattern);
337                }
338
339                // Find OPTIONAL block
340                let after_optional = &pattern_text[abs_pos + 8..];
341                if let Some(opt_brace) = after_optional.find('{') {
342                    if let Some(opt_end) = find_matching_brace(after_optional, opt_brace) {
343                        let optional_content = &after_optional[opt_brace + 1..opt_end];
344
345                        if let Some(opt_pattern) = parse_simple_pattern(optional_content) {
346                            groups.push(PatternGroup {
347                                patterns: vec![opt_pattern],
348                                optional: true,
349                            });
350                        }
351
352                        pos = abs_pos + 8 + opt_end + 1;
353                    } else {
354                        break;
355                    }
356                } else {
357                    break;
358                }
359            } else {
360                // No more OPTIONAL
361                if let Some(req_pattern) = parse_simple_pattern(&pattern_text[pos..]) {
362                    required_patterns.push(req_pattern);
363                }
364                break;
365            }
366        }
367
368        if !required_patterns.is_empty() {
369            groups.push(PatternGroup {
370                patterns: required_patterns,
371                optional: false,
372            });
373        }
374    } else {
375        // No OPTIONAL - simple pattern
376        if let Some(pattern) = parse_simple_pattern(pattern_text) {
377            groups.push(PatternGroup {
378                patterns: vec![pattern],
379                optional: false,
380            });
381        }
382    }
383
384    if groups.is_empty() {
385        Ok(None)
386    } else {
387        Ok(Some(groups))
388    }
389}
390
391/// Execute a SELECT query with UNION (needs to be implemented in RdfStore)
392/// This is a placeholder - actual implementation stays in RdfStore
393pub fn execute_union_query_placeholder() {
394    // This function signature is here for reference
395    // The actual execute_union_query must stay in RdfStore because it needs access to self
396}