1use crate::lexicon::generate_embedded_triple_id;
7
8use crate::sparql_ast::{BindingRow, SparqlQuery};
9use crate::sparql_executor::QueryExecutor;
10use crate::sparql_parser;
11use crate::sparql_planner::QueryPlanner;
12use crate::webizen_bytecode::ExecutionStats;
13use crate::NQuin;
14
15pub const QUERY_OUT_SLOTS: usize = 1_000;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum QueryExecError {
19 EmptyQuery,
20 ParseError(String),
21 OutputBufferFull,
22 InvalidProgram,
23 ClassifiedEgress,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum QueryEngine {
28 NTriplesPattern,
29 SparqlLibrary,
30}
31
32#[derive(Debug, Clone)]
33pub struct SparqlQueryStats {
34 pub binding_count: usize,
35 pub ask_result: Option<bool>,
36}
37
38pub fn detect_query_engine(query: &str) -> QueryEngine {
40 let trimmed = query.trim();
41 let upper = trimmed.to_ascii_uppercase();
42 if upper.starts_with("SELECT")
43 || upper.starts_with("ASK")
44 || upper.starts_with("CONSTRUCT")
45 || upper.starts_with("DESCRIBE")
46 || upper.starts_with("PREFIX")
47 || trimmed.contains("<<")
48 {
49 QueryEngine::SparqlLibrary
50 } else {
51 QueryEngine::NTriplesPattern
52 }
53}
54
55pub fn execute_query_on_graph(
57 query: &str,
58 graph: &[NQuin],
59) -> Result<(ExecutionStats, Vec<NQuin>), QueryExecError> {
60 match detect_query_engine(query) {
61 QueryEngine::NTriplesPattern => execute_ntriples_pattern_on_graph(query, graph),
62 QueryEngine::SparqlLibrary => execute_sparql_on_graph(query, graph),
63 }
64}
65
66pub fn execute_ntriples_pattern_on_graph(
68 query: &str,
69 graph: &[NQuin],
70) -> Result<(ExecutionStats, Vec<NQuin>), QueryExecError> {
71 let trimmed = query.trim();
72 if trimmed.is_empty() {
73 return Err(QueryExecError::EmptyQuery);
74 }
75
76 let mut program = [0u8; 1024];
77 if let Err(parse_err) =
78 crate::mini_parser::compile_ntriples_to_bytecode(trimmed.as_bytes(), &mut program)
79 {
80 return Err(QueryExecError::ParseError(format!("{parse_err:?}")));
81 }
82
83 let mut out_buffer = vec![NQuin::default(); QUERY_OUT_SLOTS];
84 let stats =
85 crate::webizen_bytecode::execute_program_with_stats(&program, graph, &mut out_buffer, None)
86 .map_err(|e| match e {
87 crate::webizen_bytecode::VmError::OutputBufferFull => {
88 QueryExecError::OutputBufferFull
89 }
90 crate::webizen_bytecode::VmError::InvalidProgram => QueryExecError::InvalidProgram,
91 crate::webizen_bytecode::VmError::HaltViolation => QueryExecError::InvalidProgram,
92 })?;
93
94 let results = out_buffer[..stats.match_count].to_vec();
95 filter_classified(results, stats)
96}
97
98pub fn execute_sparql_on_graph(
100 query: &str,
101 graph: &[NQuin],
102) -> Result<(ExecutionStats, Vec<NQuin>), QueryExecError> {
103 let trimmed = query.trim();
104 if trimmed.is_empty() {
105 return Err(QueryExecError::EmptyQuery);
106 }
107
108 let (sparql_query, ctx, literals) =
109 sparql_parser::parse_sparql_full(trimmed).map_err(|e| QueryExecError::ParseError(e))?;
110 let plan =
111 QueryPlanner::plan(&sparql_query, &ctx).map_err(|e| QueryExecError::ParseError(e))?;
112 let lex_closure = |h: u64| crate::daemon_graph::graph_lexicon_lookup(h);
116 let now_ms = std::time::SystemTime::now()
122 .duration_since(std::time::UNIX_EPOCH)
123 .map(|d| d.as_millis() as u64)
124 .unwrap_or(0);
125 let seed = now_ms
126 .wrapping_mul(0x9E37_79B9_7F4A_7C15)
127 .wrapping_add(trimmed.len() as u64 + 1);
128 let sink = crate::sparql_ast::StringSink::new();
129 let resolver = crate::sparql_ast::TextResolver::with_lexicon(&literals, &lex_closure)
130 .with_sink(&sink)
131 .with_env(now_ms, seed);
132 let executor = QueryExecutor::with_resolver(graph, resolver);
133
134 let stats = ExecutionStats {
135 match_count: 0,
136 vm_cycles: 0,
137 direct_jump_ops: 0,
138 lexicon_lookup_ops: 0,
139 };
140
141 match &sparql_query {
142 SparqlQuery::Ask(_) => {
143 let ok = executor
144 .execute_ask(&plan, &ctx)
145 .map_err(|e| QueryExecError::ParseError(e))?;
146 let mut results = Vec::new();
147 if ok {
148 results.push(synthetic_binding_quin(1, 0, 0));
149 }
150 let mut out_stats = stats;
151 out_stats.match_count = results.len();
152 filter_classified(results, out_stats)
153 }
154 _ => {
155 let bindings = executor
156 .execute(&plan, &ctx)
157 .map_err(|e| QueryExecError::ParseError(e))?;
158 let results = bindings_to_quins(&bindings);
159 let mut out_stats = stats;
160 out_stats.match_count = results.len();
161 filter_classified(results, out_stats)
162 }
163 }
164}
165
166pub fn execute_ntriples_metrics(
168 query: &str,
169 graph: &[NQuin],
170) -> Result<ExecutionStats, QueryExecError> {
171 let (stats, _) = execute_query_on_graph(query, graph)?;
172 Ok(stats)
173}
174
175fn bindings_to_quins(bindings: &[BindingRow]) -> Vec<NQuin> {
176 bindings
177 .iter()
178 .map(|row| {
179 synthetic_binding_quin(
180 row.slots[0].unwrap_or(0),
181 row.slots[1].unwrap_or(0),
182 row.slots[2].unwrap_or(0),
183 )
184 })
185 .collect()
186}
187
188fn synthetic_binding_quin(subject: u64, predicate: u64, object: u64) -> NQuin {
189 let mut q = NQuin {
190 subject,
191 predicate,
192 object,
193 context: 0,
194 metadata: 0,
195 parity: 0,
196 };
197 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
198 q
199}
200
201fn filter_classified(
202 results: Vec<NQuin>,
203 stats: ExecutionStats,
204) -> Result<(ExecutionStats, Vec<NQuin>), QueryExecError> {
205 for quin in &results {
206 if quin.get_sensitivity_byte() == NQuin::SENSITIVITY_CLASSIFIED {
207 return Err(QueryExecError::ClassifiedEgress);
208 }
209 }
210 Ok((stats, results))
211}
212
213pub fn make_star_annotation(
215 subject: u64,
216 predicate: u64,
217 object: u64,
218 ann_predicate: u64,
219 ann_object: u64,
220) -> [NQuin; 2] {
221 let base = synthetic_binding_quin(subject, predicate, object);
222 let virtual_id = generate_embedded_triple_id(subject, predicate, object);
223 let annotation = synthetic_binding_quin(virtual_id, ann_predicate, ann_object);
224 [base, annotation]
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::q_hash;
231
232 #[test]
233 fn detects_sparql_select_engine() {
234 assert_eq!(
235 detect_query_engine("SELECT ?s WHERE { ?s ?p ?o }"),
236 QueryEngine::SparqlLibrary
237 );
238 }
239
240 #[test]
241 fn detects_ntriples_pattern_engine() {
242 assert_eq!(
243 detect_query_engine("<s> <p> <o> ."),
244 QueryEngine::NTriplesPattern
245 );
246 }
247
248 #[test]
249 fn sparql_select_returns_bindings() {
250 let s = q_hash("alice");
251 let p = q_hash("knows");
252 let o = q_hash("bob");
253 let graph = vec![synthetic_binding_quin(s, p, o)];
254 let query = "SELECT ?x WHERE { ?x ?p ?o }";
255 let (_, results) = execute_sparql_on_graph(query, &graph).expect("sparql");
256 assert!(!results.is_empty());
257 assert_eq!(results[0].subject, s);
258 }
259
260 #[test]
261 fn sparql_geo_filter_constant_geometry() {
262 let s = q_hash("place1");
266 let p = q_hash("hasLoc");
267 let o = q_hash("loc1");
268 let graph = vec![synthetic_binding_quin(s, p, o)];
269
270 let pass = "SELECT ?s WHERE { ?s ?p ?o . \
271 FILTER(geof:distance(\"POINT(0 0)\", \"POINT(0 1)\") < 200000) }";
272 let (_, r) = execute_sparql_on_graph(pass, &graph).expect("geo distance filter");
273 assert_eq!(r.len(), 1, "~111 km < 200 km → row passes");
274
275 let fail = "SELECT ?s WHERE { ?s ?p ?o . \
276 FILTER(geof:distance(\"POINT(0 0)\", \"POINT(0 1)\") > 200000) }";
277 let (_, r2) = execute_sparql_on_graph(fail, &graph).expect("geo distance filter");
278 assert_eq!(r2.len(), 0, "~111 km not > 200 km → row pruned");
279 }
280
281 #[test]
282 fn sparql_geo_variable_geometry_via_lexicon() {
283 use crate::sparql_executor::QueryExecutor;
287 use crate::sparql_planner::QueryPlanner;
288
289 let place = q_hash("place");
290 let has_loc = crate::lexicon::generate_60bit_token(b"hasLoc");
291 let loc_hash = crate::lexicon::generate_60bit_token(b"POINT(2 2)");
293 let graph = vec![synthetic_binding_quin(place, has_loc, loc_hash)];
294
295 let query = "SELECT ?s WHERE { ?s <hasLoc> ?loc . \
296 FILTER(geof:sfWithin(?loc, \"POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))\")) }";
297 let (sparql_query, ctx, literals) =
298 crate::sparql_parser::parse_sparql_full(query).expect("parse");
299 let plan = QueryPlanner::plan(&sparql_query, &ctx).expect("plan");
300
301 let lex = |h: u64| -> Option<String> {
302 if h == loc_hash {
303 Some("POINT(2 2)".to_string())
304 } else {
305 None
306 }
307 };
308 let resolver = crate::sparql_ast::TextResolver::with_lexicon(&literals, &lex);
309 let executor = QueryExecutor::with_resolver(&graph, resolver);
310 let bindings = executor.execute(&plan, &ctx).expect("execute");
311 assert_eq!(
312 bindings.len(),
313 1,
314 "point (2,2) is within the square → 1 row"
315 );
316 }
317
318 #[test]
319 fn sparql_geo_sfwithin_constant_geometry() {
320 let s = q_hash("place2");
322 let graph = vec![synthetic_binding_quin(s, q_hash("p"), q_hash("o"))];
323 let inside = "SELECT ?s WHERE { ?s ?p ?o . \
324 FILTER(geof:sfWithin(\"POINT(2 2)\", \"POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))\")) }";
325 let (_, r) = execute_sparql_on_graph(inside, &graph).expect("sfWithin");
326 assert_eq!(r.len(), 1, "point (2,2) is within the square");
327
328 let outside = "SELECT ?s WHERE { ?s ?p ?o . \
329 FILTER(geof:sfWithin(\"POINT(9 9)\", \"POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))\")) }";
330 let (_, r2) = execute_sparql_on_graph(outside, &graph).expect("sfWithin");
331 assert_eq!(r2.len(), 0, "point (9,9) is outside the square");
332 }
333
334 #[test]
335 fn sparql_local_service_executes_inner() {
336 let s = q_hash("alice");
339 let p = crate::lexicon::generate_60bit_token(b"knows");
340 let o = q_hash("bob");
341 let graph = vec![synthetic_binding_quin(s, p, o)];
342 let query = "SELECT ?s WHERE { SERVICE <local:graph> { ?s <knows> ?o } }";
343 let (_, results) = execute_sparql_on_graph(query, &graph).expect("service query");
344 assert_eq!(
345 results.len(),
346 1,
347 "local SERVICE should run the inner pattern"
348 );
349 }
350
351 #[test]
352 fn sparql_union_combines_both_branches() {
353 let s_alice = q_hash("alice");
356 let s_bob = q_hash("bob");
357 let p_knows = crate::lexicon::generate_60bit_token(b"knows");
358 let p_likes = crate::lexicon::generate_60bit_token(b"likes");
359 let o = q_hash("carol");
360 let graph = vec![
361 synthetic_binding_quin(s_alice, p_knows, o),
362 synthetic_binding_quin(s_bob, p_likes, o),
363 ];
364 let query = "SELECT ?s WHERE { { ?s <knows> ?o } UNION { ?s <likes> ?o } }";
365 let (_, results) = execute_sparql_on_graph(query, &graph).expect("union query");
366 assert_eq!(
367 results.len(),
368 2,
369 "UNION should return rows from both branches"
370 );
371 }
372
373 #[test]
374 fn sparql_filter_numeric_prunes_rows() {
375 let s_alice = q_hash("alice");
379 let s_bob = q_hash("bob");
380 let pred = crate::lexicon::generate_60bit_token(b"age");
381 let graph = vec![
382 synthetic_binding_quin(s_alice, pred, 20),
383 synthetic_binding_quin(s_bob, pred, 10),
384 ];
385 let query = "SELECT ?s WHERE { ?s <age> ?o . FILTER(?o >= 18) }";
386 let (_, results) = execute_sparql_on_graph(query, &graph).expect("filter query");
387 assert_eq!(results.len(), 1, "FILTER should prune bob (age 10)");
388 }
389
390 #[test]
391 fn sparql_star_annotation_query() {
392 let s = q_hash("alice");
393 let p = q_hash("knows");
394 let o = q_hash("bob");
395 let ann_p = q_hash("certainty");
396 let ann_o = 95;
397 let quins = make_star_annotation(s, p, o, ann_p, ann_o);
398 let query = "SELECT ?innerS ?val WHERE { << ?innerS ?innerP ?innerO >> ?annP ?val }";
399 let (_, results) = execute_sparql_on_graph(query, &quins).expect("star query");
400 assert!(!results.is_empty());
401 assert_eq!(results[0].subject, s);
402 assert_eq!(results[0].predicate, ann_o);
403 }
404}