Skip to main content

qualia_core_db/services/
daemon_query.rs

1//! Shared query execution for HTTP `/query`, MCP `query_sparql`, and WS `/qualia-bridge`.
2//!
3//! Routes full SPARQL (SELECT/ASK/PREFIX/RDF-Star) through `sparql_library`;
4//! simple N-Triples patterns continue to use the bytecode fast path.
5
6use 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
38/// Detect whether a query should use the SPARQL library instead of the bytecode VM.
39pub 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
55/// Unified graph query entry point.
56pub 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
66/// Compile and run a single N-Triples pattern against `graph`.
67pub 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
98/// Execute SPARQL SELECT/ASK via the library planner + executor.
99pub 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    // A text resolver carrying the query's literal constants PLUS the resident
113    // graph's ingested lexicon, so `geof:*`/text functions resolve both constant
114    // geometry and variable geometry bound from ingested data.
115    let lex_closure = |h: u64| crate::daemon_graph::graph_lexicon_lookup(h);
116    // Query-stable clock + seed + produced-string sink for the value-producing and
117    // temporal FILTER/BIND builtins (NOW / CONCAT / SUBSTR / UUID / … — QISP-R06).
118    // `now_ms` is captured ONCE per execution so NOW is stable within the query (plan
119    // §4.4 referential transparency); the seed varies per execution so UUID varies across
120    // queries but is stable within one.
121    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
166/// Metrics-only path for WebSocket benchmarks (no result serialisation).
167pub 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
213/// Build RDF-Star annotation quins for tests: `<<s p o>> ann pred val`.
214pub 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        // End-to-end GeoSPARQL: a FILTER over geof:distance with constant WKT
263        // literals — parsed to Function::Custom, resolved via the query-literal
264        // table, and computed by the real haversine engine. (0,0)→(0,1) ≈ 111 km.
265        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        // The common GeoSPARQL pattern: FILTER(geof:sfWithin(?loc, <const polygon>))
284        // where ?loc binds to an ingested geometry literal. Proves the variable
285        // side resolves through a lexicon closure (as a Q42LexMmap would supply).
286        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        // The object is the geometry-literal hash; the lexicon maps it to WKT.
292        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        // geof:sfWithin over two constant geometries: a point inside a polygon.
321        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        // End-to-end: `SERVICE <local:…> { … }` (grammar slice 3) runs the inner
337        // pattern against the local graph via the executor's Service operator.
338        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        // End-to-end: `{ … } UNION { … }` now parses (grammar slice 2) and the
354        // executor's Union operator returns rows from both branches.
355        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        // End-to-end: a real FILTER clause is now parsed (grammar slice 1) and
376        // evaluated by the existing executor. alice(age 20) passes `?o >= 18`,
377        // bob(age 10) is pruned.
378        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}