1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! CONSTRUCT query executor
//!
//! Provides specialized operations for SPARQL CONSTRUCT queries,
//! including execution and materialization (inserting results back into the graph).
use crate::graph::Graph;
use crate::utils::error::{Error, Result};
use oxigraph::sparql::QueryResults;
use sha2::{Digest, Sha256};
/// Executor for SPARQL CONSTRUCT queries
pub struct ConstructExecutor<'a> {
/// Reference to the graph
graph: &'a Graph,
}
impl<'a> ConstructExecutor<'a> {
/// Create a new CONSTRUCT executor for the given graph
///
/// # Arguments
/// * `graph` - Reference to the RDF graph
pub fn new(graph: &'a Graph) -> Self {
Self { graph }
}
/// Execute a CONSTRUCT query and return the resulting triples as strings
///
/// # Arguments
/// * `query` - SPARQL CONSTRUCT query string
///
/// # Returns
/// * `Ok(Vec<String>)` - Resulting triples in N-Triples format
/// * `Err(Error)` - Query execution error
///
/// # Example
/// ```rust,no_run
/// use crate::graph::{Graph, ConstructExecutor};
///
/// let graph = Graph::new()?;
/// graph.insert_turtle(r#"
/// @prefix ex: <http://example.org/> .
/// ex:alice ex:knows ex:bob .
/// "#)?;
///
/// let executor = ConstructExecutor::new(&graph);
/// let triples = executor.execute(r#"
/// CONSTRUCT { ?s ?p ?o }
/// WHERE { ?s ?p ?o }
/// "#)?;
/// # Ok::<(), crate::utils::error::Error>(())
/// ```
pub fn execute(&self, query: &str) -> Result<Vec<String>> {
// Execute query using the graph's query method
let results = self
.graph
.query(query)
.map_err(|e| Error::new(&format!("CONSTRUCT query failed: {}", e)))?;
// Handle CONSTRUCT results (Graph variant)
match results {
QueryResults::Graph(quads) => {
let mut triples = Vec::new();
for quad_result in quads {
let quad = quad_result
.map_err(|e| Error::new(&format!("Error reading quad: {}", e)))?;
triples.push(quad.to_string());
}
Ok(triples)
}
QueryResults::Solutions(_) => Err(Error::new(
"Expected CONSTRUCT query but got SELECT results",
)),
QueryResults::Boolean(_) => {
Err(Error::new("Expected CONSTRUCT query but got ASK results"))
}
}
}
/// Execute a CONSTRUCT query and insert the results back into the graph
///
/// This implements the "sequential materialization" pattern where each
/// CONSTRUCT rule's output is materialized before the next rule executes.
///
/// # Arguments
/// * `query` - SPARQL CONSTRUCT query string
///
/// # Returns
/// * `Ok(usize)` - Number of triples added
/// * `Err(Error)` - Query or insert error
pub fn execute_and_materialize(&self, query: &str) -> Result<usize> {
// First execute the CONSTRUCT
let triples = self.execute(query)?;
let count = triples.len();
// Convert triples to N-Triples format and insert via Turtle
// Note: Quad::to_string() produces N-Quads format WITHOUT trailing dot
// (e.g. "<s> <p> <o>"). We must append " ." to each line so the Turtle
// parser (which is a superset of N-Triples) can accept them.
if !triples.is_empty() {
let ntriples: String = triples
.iter()
.map(|t| format!("{} .", t))
.collect::<Vec<_>>()
.join("\n");
self.graph.insert_turtle(&ntriples)?;
}
Ok(count)
}
/// Execute a chain of CONSTRUCT queries in order, materializing after each
///
/// # Arguments
/// * `queries` - Ordered list of (name, query) pairs
///
/// # Returns
/// * `Ok(Vec<(String, usize)>)` - List of (rule_name, triples_added)
/// * `Err(Error)` - First error encountered
pub fn execute_chain(&self, queries: &[(&str, &str)]) -> Result<Vec<(String, usize)>> {
let mut results = Vec::new();
for (name, query) in queries {
let count = self.execute_and_materialize(query)?;
results.push((name.to_string(), count));
}
Ok(results)
}
/// Generate a deterministic IRI from a base and optional salt
///
/// This ensures reproducible IRI generation across runs.
///
/// # Arguments
/// * `base` - Base IRI prefix
/// * `components` - Components to include in hash
/// * `salt` - Optional salt for additional uniqueness
///
/// # Returns
/// A deterministic IRI string
pub fn generate_iri(base: &str, components: &[&str], salt: Option<&str>) -> String {
let mut hasher = Sha256::new();
// Include base in hash
hasher.update(base.as_bytes());
// Include all components
for component in components {
hasher.update(component.as_bytes());
}
// Include salt if provided
if let Some(s) = salt {
hasher.update(s.as_bytes());
}
let hash = hasher.finalize();
// Use first 16 bytes (32 hex chars) for reasonably unique IRI
let hash_str: String = hash.iter().take(16).map(|b| format!("{:02x}", b)).collect();
format!("{}{}", base, hash_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_iri_deterministic() {
let iri1 = ConstructExecutor::generate_iri(
"http://ggen.dev/code#",
&["User", "struct"],
Some("salt-v1"),
);
let iri2 = ConstructExecutor::generate_iri(
"http://ggen.dev/code#",
&["User", "struct"],
Some("salt-v1"),
);
let iri3 = ConstructExecutor::generate_iri(
"http://ggen.dev/code#",
&["Order", "struct"],
Some("salt-v1"),
);
// Same inputs produce same output
assert_eq!(iri1, iri2);
// Different inputs produce different output
assert_ne!(iri1, iri3);
}
#[test]
fn test_generate_iri_format() {
let iri = ConstructExecutor::generate_iri("http://example.org/", &["test"], None);
assert!(iri.starts_with("http://example.org/"));
// Hash portion should be 32 hex chars
let hash_part = iri.strip_prefix("http://example.org/").unwrap();
assert_eq!(hash_part.len(), 32);
}
#[test]
fn test_construct_executor_creation() {
let graph = Graph::new().expect("Failed to create graph");
let _executor = ConstructExecutor::new(&graph);
// Just verify construction works
}
#[test]
fn test_execute_simple_construct() {
let graph = Graph::new().expect("Failed to create graph");
graph
.insert_turtle(
r#"
@prefix ex: <http://example.org/> .
ex:alice ex:knows ex:bob .
"#,
)
.expect("Failed to insert turtle");
let executor = ConstructExecutor::new(&graph);
let result = executor.execute(
r#"
PREFIX ex: <http://example.org/>
CONSTRUCT { ?s ex:related ?o }
WHERE { ?s ex:knows ?o }
"#,
);
assert!(result.is_ok());
let triples = result.unwrap();
assert_eq!(triples.len(), 1);
}
}