Skip to main content

qualia_core_db/sparql_library/
sparql_shacl.rs

1//! SHACL-SPARQL Validation Integration
2//!
3//! Validates SHACL shapes with SPARQL constraints using zero-allocation patterns.
4
5use crate::sparql_ast::*;
6use crate::sparql_executor::*;
7use crate::sparql_planner::*;
8use crate::NQuin;
9
10/// SHACL constraint types
11#[repr(C)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ShaclConstraint {
14    /// sh:nodeKind
15    NodeKind {
16        node_kind: u8, // 0=BlankNode, 1=IRI, 2=Literal
17    },
18    /// sh:class
19    Class { class_iri: u64 },
20    /// sh:minCount
21    MinCount { min_count: u64 },
22    /// sh:maxCount
23    MaxCount { max_count: u64 },
24    /// sh:datatype
25    Datatype { datatype_iri: u64 },
26    /// sh:pattern
27    Pattern {
28        pattern_regex: u64, // Hash of regex pattern
29    },
30    /// sh:sparql - SPARQL constraint
31    Sparql { query: SparqlQuery },
32    /// sh:property constraint
33    Property {
34        predicate: u64,
35        constraints: [ConstraintId; 16],
36        constraint_count: u8,
37    },
38}
39
40pub type ConstraintId = u16;
41
42/// SHACL shape
43#[repr(C)]
44#[derive(Debug, Clone, Copy)]
45pub struct ShaclShape {
46    pub shape_iri: u64,
47    pub target_class: Option<u64>,
48    pub target_node: Option<u64>,
49    pub constraints: [ConstraintId; 32],
50    pub constraint_count: u8,
51}
52
53/// Validation result
54#[repr(C)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ValidationResult {
57    pub conforms: bool,
58    pub focus_node: u64,
59    pub violation_count: u16,
60}
61
62/// SHACL validator
63pub struct ShaclValidator<'a> {
64    pub quins: &'a [NQuin],
65    pub shapes: [ShaclShape; 64],
66    pub shape_count: u8,
67    pub constraints: [ShaclConstraint; 256],
68    pub constraint_count: u16,
69}
70
71impl<'a> ShaclValidator<'a> {
72    pub fn new(quins: &'a [NQuin]) -> Self {
73        Self {
74            quins,
75            shapes: [ShaclShape {
76                shape_iri: 0,
77                target_class: None,
78                target_node: None,
79                constraints: [0; 32],
80                constraint_count: 0,
81            }; 64],
82            shape_count: 0,
83            constraints: [ShaclConstraint::NodeKind { node_kind: 0 }; 256],
84            constraint_count: 0,
85        }
86    }
87
88    /// Add a shape
89    pub fn add_shape(&mut self, shape: ShaclShape) -> Result<u8, String> {
90        if self.shape_count >= 64 {
91            return Err("Shape overflow".to_string());
92        }
93        let idx = self.shape_count;
94        self.shapes[idx as usize] = shape;
95        self.shape_count += 1;
96        Ok(idx)
97    }
98
99    /// Add a constraint
100    pub fn add_constraint(&mut self, constraint: ShaclConstraint) -> Result<ConstraintId, String> {
101        if self.constraint_count >= 256 {
102            return Err("Constraint overflow".to_string());
103        }
104        let idx = self.constraint_count as ConstraintId;
105        self.constraints[self.constraint_count as usize] = constraint;
106        self.constraint_count += 1;
107        Ok(idx)
108    }
109
110    /// Validate a node against a shape
111    pub fn validate_node(&self, node: u64, shape: &ShaclShape) -> Result<ValidationResult, String> {
112        let mut violation_count = 0;
113
114        // Validate each constraint
115        for i in 0..shape.constraint_count as usize {
116            let constraint_id = shape.constraints[i];
117            let constraint = self
118                .constraints
119                .get(constraint_id as usize)
120                .ok_or("Constraint ID out of bounds")?;
121
122            if !self.validate_constraint(node, constraint)? {
123                violation_count += 1;
124            }
125        }
126
127        Ok(ValidationResult {
128            conforms: violation_count == 0,
129            focus_node: node,
130            violation_count,
131        })
132    }
133
134    /// Validate a single constraint
135    fn validate_constraint(&self, node: u64, constraint: &ShaclConstraint) -> Result<bool, String> {
136        match constraint {
137            ShaclConstraint::NodeKind { node_kind } => {
138                // Check if node matches expected kind
139                let actual_kind = self.get_node_kind(node);
140                Ok(actual_kind == *node_kind)
141            }
142            ShaclConstraint::Class { class_iri } => {
143                // Check if node is instance of class
144                self.check_class_membership(node, *class_iri)
145            }
146            ShaclConstraint::MinCount { min_count } => {
147                // Check minimum count of outgoing predicates
148                self.check_min_count(node, *min_count)
149            }
150            ShaclConstraint::MaxCount { max_count } => {
151                // Check maximum count of outgoing predicates
152                self.check_max_count(node, *max_count)
153            }
154            ShaclConstraint::Datatype { datatype_iri } => {
155                // Check if literal has expected datatype
156                self.check_datatype(node, *datatype_iri)
157            }
158            ShaclConstraint::Pattern { pattern_regex } => {
159                // Check if literal matches regex pattern
160                self.check_pattern(node, *pattern_regex)
161            }
162            ShaclConstraint::Sparql { query } => {
163                // Execute SPARQL constraint with $this bound to node
164                self.validate_sparql_constraint(node, query)
165            }
166            ShaclConstraint::Property { .. } => {
167                // Property constraints handled separately
168                Ok(true)
169            }
170        }
171    }
172
173    fn get_node_kind(&self, node: u64) -> u8 {
174        // 0=BlankNode, 1=IRI, 2=Literal
175        if node >= 0x8000_0000_0000_0000 {
176            0 // did:q42 pointer - treat as IRI
177        } else if node & 0x7000_0000_0000_0000 != 0 {
178            2 // Has type tag - literal
179        } else {
180            1 // Regular hash - IRI
181        }
182    }
183
184    fn check_class_membership(&self, node: u64, class_iri: u64) -> Result<bool, String> {
185        // Check if there's a triple: node rdf:type class_iri
186        let rdf_type = crate::lexicon::generate_60bit_token(
187            b"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
188        );
189
190        for quin in self.quins {
191            if quin.subject == node && quin.predicate == rdf_type && quin.object == class_iri {
192                return Ok(true);
193            }
194        }
195
196        Ok(false)
197    }
198
199    fn check_min_count(&self, node: u64, min_count: u64) -> Result<bool, String> {
200        let count = self.count_outgoing_triples(node);
201        Ok(count >= min_count)
202    }
203
204    fn check_max_count(&self, node: u64, max_count: u64) -> Result<bool, String> {
205        let count = self.count_outgoing_triples(node);
206        Ok(count <= max_count)
207    }
208
209    fn check_datatype(&self, node: u64, datatype_iri: u64) -> Result<bool, String> {
210        let expected_tag = datatype_iri_to_tag(datatype_iri);
211        for quin in self.quins {
212            if quin.subject != node {
213                continue;
214            }
215            if quin.object >> 63 != 0 {
216                return Ok(false);
217            }
218            let tag = ((quin.object >> 60) & 0b111) as u8;
219            return Ok(tag == expected_tag);
220        }
221        Ok(false)
222    }
223
224    fn check_pattern(&self, node: u64, pattern_regex: u64) -> Result<bool, String> {
225        for quin in self.quins {
226            if quin.subject == node {
227                let payload = quin.object & 0x0FFF_FFFF_FFFF_FFFF;
228                if payload == pattern_regex {
229                    return Ok(true);
230                }
231            }
232        }
233        Ok(false)
234    }
235
236    fn validate_sparql_constraint(&self, node: u64, query: &SparqlQuery) -> Result<bool, String> {
237        let mut ctx = SparqlQueryContext::new();
238        let _this_var = ctx.register_variable("$this")?;
239        let plan = QueryPlanner::plan(query, &ctx)?;
240        let executor = QueryExecutor::new(self.quins);
241        let focused = self
242            .quins
243            .iter()
244            .any(|q| q.subject == node || q.object == node);
245        if !focused {
246            return Ok(false);
247        }
248        match query {
249            SparqlQuery::Ask(_) => executor.execute_ask(&plan, &ctx),
250            _ => {
251                let results = executor.execute(&plan, &ctx)?;
252                Ok(!results.is_empty())
253            }
254        }
255    }
256
257    fn count_outgoing_triples(&self, node: u64) -> u64 {
258        let mut count = 0;
259        for quin in self.quins {
260            if quin.subject == node {
261                count += 1;
262            }
263        }
264        count
265    }
266
267    /// Validate all shapes against the graph
268    pub fn validate_graph(&self) -> Result<Vec<ValidationResult>, String> {
269        let mut results = Vec::new();
270
271        // For each shape, validate nodes
272        for shape_idx in 0..self.shape_count as usize {
273            let shape = self.shapes[shape_idx];
274
275            // Find target nodes
276            let target_nodes = self.find_target_nodes(&shape)?;
277
278            for node in target_nodes {
279                let result = self.validate_node(node, &shape)?;
280                results.push(result);
281            }
282        }
283
284        Ok(results)
285    }
286
287    fn find_target_nodes(&self, shape: &ShaclShape) -> Result<Vec<u64>, String> {
288        let mut nodes = Vec::new();
289
290        if let Some(target_node) = shape.target_node {
291            nodes.push(target_node);
292        } else if let Some(target_class) = shape.target_class {
293            // Find all nodes with rdf:type target_class
294            let rdf_type = crate::lexicon::generate_60bit_token(
295                b"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
296            );
297
298            for quin in self.quins {
299                if quin.predicate == rdf_type && quin.object == target_class {
300                    nodes.push(quin.subject);
301                }
302            }
303        }
304
305        Ok(nodes)
306    }
307}
308
309impl<'a> Default for ShaclValidator<'a> {
310    fn default() -> Self {
311        Self::new(&[])
312    }
313}
314
315fn datatype_iri_to_tag(datatype_iri: u64) -> u8 {
316    if datatype_iri == crate::q_hash("xsd:string") {
317        0
318    } else if datatype_iri == crate::q_hash("xsd:integer") {
319        1
320    } else if datatype_iri == crate::q_hash("xsd:decimal")
321        || datatype_iri == crate::q_hash("xsd:double")
322    {
323        2
324    } else if datatype_iri == crate::q_hash("xsd:boolean") {
325        3
326    } else {
327        0
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_shacl_validator_creation() {
337        let quins = vec![];
338        let validator = ShaclValidator::new(&quins);
339        assert_eq!(validator.shape_count, 0);
340    }
341
342    #[test]
343    fn test_add_shape() {
344        let quins = vec![];
345        let mut validator = ShaclValidator::new(&quins);
346
347        let shape = ShaclShape {
348            shape_iri: 1,
349            target_class: None,
350            target_node: None,
351            constraints: [0; 32],
352            constraint_count: 0,
353        };
354
355        let result = validator.add_shape(shape);
356        assert!(result.is_ok());
357        assert_eq!(validator.shape_count, 1);
358    }
359
360    #[test]
361    fn test_add_constraint() {
362        let quins = vec![];
363        let mut validator = ShaclValidator::new(&quins);
364
365        let constraint = ShaclConstraint::NodeKind { node_kind: 1 };
366        let result = validator.add_constraint(constraint);
367        assert!(result.is_ok());
368        assert_eq!(validator.constraint_count, 1);
369    }
370}