Skip to main content

qualia_core_db/sparql_library/
sparql_websocket.rs

1//! SPARQL WebSocket Support
2//!
3//! Real-time SPARQL query results via WebSocket using zero-allocation patterns.
4
5use crate::sparql_ast::*;
6use crate::sparql_executor::*;
7use crate::sparql_library::serialisers::sparql_results::ResultFormatter;
8use crate::sparql_parser;
9use crate::sparql_planner::*;
10use crate::NQuin;
11
12use std::collections::HashMap;
13
14/// WebSocket message type
15#[repr(C)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum WebSocketMessageType {
18    Query,
19    Subscribe,
20    Unsubscribe,
21    Result,
22    Error,
23    Close,
24}
25
26/// WebSocket message
27#[repr(C)]
28#[derive(Debug, Clone, Copy)]
29pub struct WebSocketMessage {
30    pub msg_type: WebSocketMessageType,
31    pub query_id: u64,
32    pub payload_len: u16,
33}
34
35/// WebSocket session
36#[repr(C)]
37#[derive(Debug, Clone, Copy)]
38pub struct WebSocketSession {
39    pub session_id: u64,
40    pub active_query: Option<u64>,
41    pub subscribed: bool,
42}
43
44/// A real-time subscription registered by a WebSocket client.
45///
46/// Each subscription binds a `client_id` (the WebSocket connection identifier)
47/// to a SPARQL query whose results the client wants pushed to it whenever the
48/// underlying data changes.
49#[derive(Debug, Clone)]
50pub struct Subscription {
51    /// Unique subscription identifier (a query-derived token).
52    pub subscription_id: String,
53    /// The WebSocket client that owns this subscription.
54    pub client_id: String,
55    /// The SPARQL query the subscription is watching.
56    pub query: String,
57}
58
59impl Subscription {
60    /// Create a new subscription.
61    pub fn new(subscription_id: String, client_id: String, query: String) -> Self {
62        Self {
63            subscription_id,
64            client_id,
65            query,
66        }
67    }
68}
69
70/// SPARQL WebSocket handler
71pub struct SparqlWebSocketHandler<'a> {
72    pub quins: &'a [NQuin],
73    pub sessions: [Option<WebSocketSession>; 32],
74    pub session_count: u8,
75    /// Active subscriptions keyed by subscription id.
76    pub subscriptions: HashMap<String, Subscription>,
77    /// Whether [`initialize`](Self::initialize) has been called.
78    pub initialized: bool,
79}
80
81impl<'a> SparqlWebSocketHandler<'a> {
82    pub fn new(quins: &'a [NQuin]) -> Self {
83        Self {
84            quins,
85            sessions: [None; 32],
86            session_count: 0,
87            subscriptions: HashMap::new(),
88            initialized: false,
89        }
90    }
91
92    /// Initialize the handler for use. Resets subscription state and marks the
93    /// handler ready. Must be called before subscribing/notifying so downstream
94    /// code can rely on a deterministic start state.
95    pub fn initialize(&mut self) -> Result<(), String> {
96        self.subscriptions.clear();
97        self.initialized = true;
98        Ok(())
99    }
100
101    /// Register a new WebSocket session
102    pub fn register_session(&mut self) -> Result<u64, String> {
103        if self.session_count >= 32 {
104            return Err("Session overflow".to_string());
105        }
106
107        let session_id = self.session_count as u64;
108        self.sessions[self.session_count as usize] = Some(WebSocketSession {
109            session_id,
110            active_query: None,
111            subscribed: false,
112        });
113        self.session_count += 1;
114
115        Ok(session_id)
116    }
117
118    /// Unregister a WebSocket session
119    pub fn unregister_session(&mut self, session_id: u64) -> Result<(), String> {
120        for i in 0..self.session_count as usize {
121            if let Some(session) = self.sessions[i] {
122                if session.session_id == session_id {
123                    self.sessions[i] = None;
124                    return Ok(());
125                }
126            }
127        }
128        Err("Session not found".to_string())
129    }
130
131    /// Handle a WebSocket query, returning formatted output (json or xml).
132    ///
133    /// This is the format-aware, session-scoped variant. The simpler
134    /// [`handle_query`](Self::handle_query) returns serialized JSON bytes.
135    pub fn handle_query_formatted(
136        &self,
137        query: &str,
138        format: &str,
139        _session_id: u64,
140    ) -> Result<String, String> {
141        // Parse query
142        let (sparql_query, ctx) = sparql_parser::parse_sparql(query)?;
143
144        // Plan query
145        let plan = QueryPlanner::plan(&sparql_query, &ctx)?;
146
147        // Execute query
148        let executor = QueryExecutor::new(self.quins);
149        let results = executor.execute(&plan, &ctx)?;
150
151        // Format results
152        match format.to_lowercase().as_str() {
153            "json" => {
154                let mut output = Vec::new();
155                let vars = match &sparql_query {
156                    SparqlQuery::Select(select) => {
157                        select.variables[..select.var_count as usize].to_vec()
158                    }
159                    _ => vec![],
160                };
161                ResultFormatter::format_json(&mut output, &vars, &results, &ctx, None)
162                    .map_err(|e| e.to_string())?;
163                Ok(String::from_utf8(output).unwrap())
164            }
165            "xml" => {
166                let mut output = Vec::new();
167                let vars = match &sparql_query {
168                    SparqlQuery::Select(select) => {
169                        select.variables[..select.var_count as usize].to_vec()
170                    }
171                    _ => vec![],
172                };
173                ResultFormatter::format_xml(&mut output, &vars, &results, &ctx, None)
174                    .map_err(|e| e.to_string())?;
175                Ok(String::from_utf8(output).unwrap())
176            }
177            _ => Err("Unsupported format. Use: xml or json".to_string()),
178        }
179    }
180
181    /// Stream query results in chunks, executing the query and formatting each
182    /// chunk as JSON. This is the query-executing variant. The simpler
183    /// [`stream_results`](Self::stream_results) splits an already-serialized
184    /// byte buffer.
185    pub fn stream_query_results(
186        &self,
187        query: &str,
188        chunk_size: usize,
189        _session_id: u64,
190    ) -> Result<Vec<String>, String> {
191        // Parse and execute query
192        let (sparql_query, ctx) = sparql_parser::parse_sparql(query)?;
193        let plan = QueryPlanner::plan(&sparql_query, &ctx)?;
194        let executor = QueryExecutor::new(self.quins);
195        let results = executor.execute(&plan, &ctx)?;
196
197        // Get variables
198        let vars = match &sparql_query {
199            SparqlQuery::Select(select) => select.variables[..select.var_count as usize].to_vec(),
200            _ => vec![],
201        };
202
203        // Chunk results
204        let mut chunks = Vec::new();
205        for chunk in results.chunks(chunk_size) {
206            let mut output = Vec::new();
207            ResultFormatter::format_json(&mut output, &vars, chunk, &ctx, None)
208                .map_err(|e| e.to_string())?;
209            chunks.push(String::from_utf8(output).unwrap());
210        }
211
212        Ok(chunks)
213    }
214
215    /// Subscribe a session to real-time updates for a query (session-based).
216    ///
217    /// This is the numeric-session variant. The string-keyed
218    /// [`subscribe`](Self::subscribe) is the subscription notification API.
219    pub fn subscribe_session(&mut self, session_id: u64, query: &str) -> Result<u64, String> {
220        // Find session
221        for i in 0..self.session_count as usize {
222            if let Some(session) = self.sessions[i] {
223                if session.session_id == session_id {
224                    // Store query hash as subscription ID
225                    let query_hash = crate::lexicon::generate_60bit_token(query.as_bytes());
226                    self.sessions[i] = Some(WebSocketSession {
227                        session_id,
228                        active_query: Some(query_hash),
229                        subscribed: true,
230                    });
231                    return Ok(query_hash);
232                }
233            }
234        }
235        Err("Session not found".to_string())
236    }
237
238    /// Unsubscribe a session from updates (session-based).
239    pub fn unsubscribe_session(&mut self, session_id: u64) -> Result<(), String> {
240        for i in 0..self.session_count as usize {
241            if let Some(session) = self.sessions[i] {
242                if session.session_id == session_id {
243                    self.sessions[i] = Some(WebSocketSession {
244                        session_id,
245                        active_query: None,
246                        subscribed: false,
247                    });
248                    return Ok(());
249                }
250            }
251        }
252        Err("Session not found".to_string())
253    }
254
255    /// Notify session subscribers of updates (session-based), returning the
256    /// session ids whose active query matches `query_hash`.
257    pub fn notify_session_subscribers(&self, query_hash: u64) -> Vec<u64> {
258        let mut subscribers = Vec::new();
259
260        for i in 0..self.session_count as usize {
261            if let Some(session) = self.sessions[i] {
262                if session.subscribed && session.active_query == Some(query_hash) {
263                    subscribers.push(session.session_id);
264                }
265            }
266        }
267
268        subscribers
269    }
270
271    // ---- Subscription-based API (client_id string keyed) ----
272    //
273    // The methods below use a `client_id: &str` / `subscription_id: &str` model
274    // that is independent of the numeric session slots above. This is the
275    // notification system used by the WebSocket subscription feature.
276
277    /// Register a subscription for a client and return the subscription id.
278    ///
279    /// The subscription id is derived from the query (via
280    /// `generate_60bit_token`) so the same query re-subscribed by the same
281    /// client yields a stable id. The query and client_id are stored in the
282    /// subscriptions map for later notification.
283    pub fn subscribe(&mut self, client_id: &str, query: &str) -> Result<String, String> {
284        let token = crate::lexicon::generate_60bit_token(query.as_bytes());
285        let subscription_id = format!("sub-{}-{}", client_id, token);
286        let subscription = Subscription::new(
287            subscription_id.clone(),
288            client_id.to_string(),
289            query.to_string(),
290        );
291        self.subscriptions
292            .insert(subscription_id.clone(), subscription);
293        Ok(subscription_id)
294    }
295
296    /// Remove a subscription by its id. Returns an error if the subscription
297    /// does not exist.
298    pub fn unsubscribe(&mut self, subscription_id: &str) -> Result<(), String> {
299        if self.subscriptions.remove(subscription_id).is_some() {
300            Ok(())
301        } else {
302            Err("Subscription not found".to_string())
303        }
304    }
305
306    /// List all active subscriptions.
307    pub fn get_subscriptions(&self) -> Vec<&Subscription> {
308        self.subscriptions.values().collect()
309    }
310
311    /// Count active subscriptions.
312    pub fn subscription_count(&self) -> usize {
313        self.subscriptions.len()
314    }
315
316    /// Notify all active subscribers of an event.
317    ///
318    /// For each subscription, returns a `(client_id, notification_message)`
319    /// pair. The notification message is a JSON-like string containing the
320    /// subscription's query and the event data, so a transport layer can push
321    /// it directly to the connected client.
322    pub fn notify_subscribers(&self, event_data: &str) -> Vec<(String, String)> {
323        let mut notifications = Vec::new();
324        for sub in self.subscriptions.values() {
325            let message = format_notification(&sub.subscription_id, &sub.query, event_data);
326            notifications.push((sub.client_id.clone(), message));
327        }
328        notifications
329    }
330
331    /// Execute a SPARQL query and return the results as serialized bytes in a
332    /// simple JSON format. This is the WebSocket query handler entry point.
333    pub fn handle_query(&self, query: &str) -> Result<Vec<u8>, String> {
334        // Parse query
335        let (sparql_query, ctx) = sparql_parser::parse_sparql(query)?;
336
337        // Plan query
338        let plan = QueryPlanner::plan(&sparql_query, &ctx)?;
339
340        // Execute query
341        let executor = QueryExecutor::new(self.quins);
342        let results = executor.execute(&plan, &ctx)?;
343
344        // Serialize results as JSON bytes.
345        let vars = match &sparql_query {
346            SparqlQuery::Select(select) => select.variables[..select.var_count as usize].to_vec(),
347            _ => vec![],
348        };
349        let mut output = Vec::new();
350        ResultFormatter::format_json(&mut output, &vars, &results, &ctx, None)
351            .map_err(|e| e.to_string())?;
352        Ok(output)
353    }
354
355    /// Split a serialized result buffer into chunks for streaming over
356    /// WebSocket frames. Each chunk is at most `chunk_size` bytes (the final
357    /// chunk may be smaller).
358    pub fn stream_results(&self, results: &[u8], chunk_size: usize) -> Vec<Vec<u8>> {
359        if chunk_size == 0 {
360            // A zero chunk size would loop forever; return the whole buffer as
361            // a single chunk instead.
362            return vec![results.to_vec()];
363        }
364        results.chunks(chunk_size).map(|c| c.to_vec()).collect()
365    }
366}
367
368impl<'a> Default for SparqlWebSocketHandler<'a> {
369    fn default() -> Self {
370        Self::new(&[])
371    }
372}
373
374/// Build a JSON-like notification message for a subscription event.
375///
376/// The message is a compact JSON object containing the subscription id, the
377/// subscribed query, and the event payload. It is intentionally simple and
378/// dependency-free so it can be pushed directly over a WebSocket frame.
379fn format_notification(subscription_id: &str, query: &str, event_data: &str) -> String {
380    format!(
381        "{{\"subscriptionId\":\"{}\",\"query\":\"{}\",\"event\":{}}}",
382        escape_json_string(subscription_id),
383        escape_json_string(query),
384        event_data
385    )
386}
387
388/// Escape a string for inclusion inside a JSON string literal. Handles the
389/// characters that are most likely to appear in subscription ids / queries.
390fn escape_json_string(s: &str) -> String {
391    let mut out = String::with_capacity(s.len());
392    for c in s.chars() {
393        match c {
394            '"' => out.push_str("\\\""),
395            '\\' => out.push_str("\\\\"),
396            '\n' => out.push_str("\\n"),
397            '\r' => out.push_str("\\r"),
398            '\t' => out.push_str("\\t"),
399            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
400            c => out.push(c),
401        }
402    }
403    out
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_websocket_handler_creation() {
412        let quins = vec![];
413        let handler = SparqlWebSocketHandler::new(&quins);
414        assert_eq!(handler.session_count, 0);
415    }
416
417    #[test]
418    fn test_register_session() {
419        let quins = vec![];
420        let mut handler = SparqlWebSocketHandler::new(&quins);
421
422        let session_id = handler.register_session().unwrap();
423        assert_eq!(session_id, 0);
424        assert_eq!(handler.session_count, 1);
425    }
426
427    #[test]
428    fn test_unregister_session() {
429        let quins = vec![];
430        let mut handler = SparqlWebSocketHandler::new(&quins);
431
432        let session_id = handler.register_session().unwrap();
433        handler.unregister_session(session_id).unwrap();
434    }
435
436    #[test]
437    fn test_subscribe_session() {
438        let quins = vec![];
439        let mut handler = SparqlWebSocketHandler::new(&quins);
440
441        let session_id = handler.register_session().unwrap();
442        let query_hash = handler
443            .subscribe_session(session_id, "SELECT ?s WHERE ?s ?p ?o")
444            .unwrap();
445
446        assert!(query_hash > 0);
447    }
448
449    // ---- Subscription-based API tests ----
450
451    #[test]
452    fn test_handler_initialize() {
453        let quins = vec![];
454        let mut handler = SparqlWebSocketHandler::new(&quins);
455        assert!(!handler.initialized);
456        handler.initialize().unwrap();
457        assert!(handler.initialized);
458        assert_eq!(handler.subscription_count(), 0);
459    }
460
461    #[test]
462    fn test_subscribe_client_returns_id() {
463        let quins = vec![];
464        let mut handler = SparqlWebSocketHandler::new(&quins);
465
466        let sub_id = handler
467            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
468            .unwrap();
469        assert!(sub_id.starts_with("sub-client-1-"));
470        assert_eq!(handler.subscription_count(), 1);
471    }
472
473    #[test]
474    fn test_subscribe_client_stores_query_and_client() {
475        let quins = vec![];
476        let mut handler = SparqlWebSocketHandler::new(&quins);
477
478        let sub_id = handler
479            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
480            .unwrap();
481        let subs = handler.get_subscriptions();
482        assert_eq!(subs.len(), 1);
483        assert_eq!(subs[0].subscription_id, sub_id);
484        assert_eq!(subs[0].client_id, "client-1");
485        assert_eq!(subs[0].query, "SELECT ?s WHERE { ?s ?p ?o }");
486    }
487
488    #[test]
489    fn test_subscribe_same_query_same_client_stable_id() {
490        let quins = vec![];
491        let mut handler = SparqlWebSocketHandler::new(&quins);
492
493        let id1 = handler
494            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
495            .unwrap();
496        let id2 = handler
497            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
498            .unwrap();
499        // Same client + same query → same id (overwrites the previous entry).
500        assert_eq!(id1, id2);
501        assert_eq!(handler.subscription_count(), 1);
502    }
503
504    #[test]
505    fn test_subscribe_different_clients_separate_subscriptions() {
506        let quins = vec![];
507        let mut handler = SparqlWebSocketHandler::new(&quins);
508
509        handler
510            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
511            .unwrap();
512        handler
513            .subscribe("client-2", "SELECT ?s WHERE { ?s ?p ?o }")
514            .unwrap();
515        assert_eq!(handler.subscription_count(), 2);
516    }
517
518    #[test]
519    fn test_unsubscribe_client_removes_subscription() {
520        let quins = vec![];
521        let mut handler = SparqlWebSocketHandler::new(&quins);
522
523        let sub_id = handler
524            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
525            .unwrap();
526        assert_eq!(handler.subscription_count(), 1);
527
528        handler.unsubscribe(&sub_id).unwrap();
529        assert_eq!(handler.subscription_count(), 0);
530        assert!(handler.get_subscriptions().is_empty());
531    }
532
533    #[test]
534    fn test_unsubscribe_client_unknown_id_errors() {
535        let quins = vec![];
536        let mut handler = SparqlWebSocketHandler::new(&quins);
537        let err = handler.unsubscribe("does-not-exist").unwrap_err();
538        assert!(err.contains("not found"));
539    }
540
541    #[test]
542    fn test_subscription_lifecycle() {
543        let quins = vec![];
544        let mut handler = SparqlWebSocketHandler::new(&quins);
545        handler.initialize().unwrap();
546
547        // Subscribe two clients.
548        let id1 = handler
549            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
550            .unwrap();
551        let id2 = handler
552            .subscribe("client-2", "SELECT ?s WHERE { ?s ?p ?o }")
553            .unwrap();
554        assert_eq!(handler.subscription_count(), 2);
555
556        // Notify both.
557        let notifications = handler.notify_subscribers("{\"type\":\"update\"}");
558        assert_eq!(notifications.len(), 2);
559        let client_ids: Vec<&str> = notifications.iter().map(|(c, _)| c.as_str()).collect();
560        assert!(client_ids.contains(&"client-1"));
561        assert!(client_ids.contains(&"client-2"));
562        for (_, msg) in &notifications {
563            assert!(msg.contains("\"query\":\"SELECT ?s WHERE { ?s ?p ?o }\""));
564            assert!(msg.contains("\"event\":{\"type\":\"update\"}"));
565        }
566
567        // Unsubscribe client-1; only client-2 remains.
568        handler.unsubscribe(&id1).unwrap();
569        assert_eq!(handler.subscription_count(), 1);
570        let notifications = handler.notify_subscribers("{\"type\":\"update\"}");
571        assert_eq!(notifications.len(), 1);
572        assert_eq!(notifications[0].0, "client-2");
573
574        // Unsubscribe client-2; none remain.
575        handler.unsubscribe(&id2).unwrap();
576        assert_eq!(handler.subscription_count(), 0);
577        assert!(handler.notify_subscribers("event").is_empty());
578    }
579
580    #[test]
581    fn test_notify_subscribers_message_format() {
582        let quins = vec![];
583        let mut handler = SparqlWebSocketHandler::new(&quins);
584        handler
585            .subscribe("client-1", "SELECT ?s WHERE { ?s ?p ?o }")
586            .unwrap();
587
588        let notifications = handler.notify_subscribers("{\"data\":42}");
589        assert_eq!(notifications.len(), 1);
590        let (_client, msg) = &notifications[0];
591        assert!(msg.starts_with("{\"subscriptionId\":\""));
592        assert!(msg.contains("\"query\":\"SELECT ?s WHERE { ?s ?p ?o }\""));
593        assert!(msg.contains("\"event\":{\"data\":42}"));
594    }
595
596    #[test]
597    fn test_handle_query_bytes_returns_json() {
598        let quins = vec![];
599        let handler = SparqlWebSocketHandler::new(&quins);
600        let bytes = handler
601            .handle_query("SELECT ?s WHERE { ?s ?p ?o }")
602            .unwrap();
603        // ResultFormatter produces a JSON document.
604        let text = String::from_utf8(bytes).unwrap();
605        assert!(text.contains("head") || text.contains("results") || text.contains('{'));
606    }
607
608    #[test]
609    fn test_handle_query_bytes_parse_error() {
610        let quins = vec![];
611        let handler = SparqlWebSocketHandler::new(&quins);
612        let err = handler.handle_query("not sparql").unwrap_err();
613        assert!(!err.is_empty());
614    }
615
616    #[test]
617    fn test_stream_results_bytes_chunks() {
618        let quins = vec![];
619        let handler = SparqlWebSocketHandler::new(&quins);
620        let data: Vec<u8> = (0..25u8).collect();
621        let chunks = handler.stream_results(&data, 10);
622        assert_eq!(chunks.len(), 3);
623        assert_eq!(chunks[0].len(), 10);
624        assert_eq!(chunks[1].len(), 10);
625        assert_eq!(chunks[2].len(), 5);
626        // Reassemble and verify round-trip.
627        let reassembled: Vec<u8> = chunks.into_iter().flatten().collect();
628        assert_eq!(reassembled, data);
629    }
630
631    #[test]
632    fn test_stream_results_bytes_empty() {
633        let quins = vec![];
634        let handler = SparqlWebSocketHandler::new(&quins);
635        let chunks = handler.stream_results(&[], 10);
636        assert!(chunks.is_empty());
637    }
638
639    #[test]
640    fn test_stream_results_bytes_zero_chunk_size() {
641        let quins = vec![];
642        let handler = SparqlWebSocketHandler::new(&quins);
643        let data: Vec<u8> = (0..5u8).collect();
644        // Zero chunk size must not panic; returns a single chunk.
645        let chunks = handler.stream_results(&data, 0);
646        assert_eq!(chunks.len(), 1);
647        assert_eq!(chunks[0], data);
648    }
649
650    #[test]
651    fn test_format_notification_escapes_quotes() {
652        let msg = format_notification("sub-1", "SELECT \"x\"", "{}");
653        assert!(msg.contains("\\\"x\\\""));
654        assert!(msg.contains("\"event\":{}"));
655    }
656
657    #[test]
658    fn test_escape_json_string() {
659        assert_eq!(escape_json_string("a\"b"), "a\\\"b");
660        assert_eq!(escape_json_string("a\\b"), "a\\\\b");
661        assert_eq!(escape_json_string("a\nb"), "a\\nb");
662        assert_eq!(escape_json_string("plain"), "plain");
663    }
664}