Skip to main content

qualia_core_db/sparql_library/parsers/
nquads_star.rs

1use crate::lexicon::{generate_60bit_token, generate_embedded_triple_id};
2use crate::rdf_star::{RdfStarParseError, RdfStarParser};
3/// N-Quads-Star Parser for QualiaDB
4///
5/// Implements RDF-Star (SPARQL 1.2) parsing for N-Quads syntax with embedded triples.
6/// N-Quads-Star extends N-Triples-Star with a fourth component (graph/context).
7/// Format: `<subject> <predicate> <object> <graph> .`
8use crate::NQuin;
9
10/// Human-readable term strings from the most recent successful parse.
11#[derive(Debug, Clone, Default)]
12pub struct NQuadsLineTerms {
13    pub subject: String,
14    pub predicate: String,
15    pub object: String,
16    pub graph: String,
17    pub outer_predicate: String,
18    pub outer_object: String,
19    pub outer_graph: String,
20}
21
22/// N-Quads-Star parser implementation
23pub struct NQuadsStarParser {
24    /// Context hash for the current parsing session
25    context_hash: u64,
26    last_terms: Option<NQuadsLineTerms>,
27}
28
29impl NQuadsStarParser {
30    /// Create a new N-Quads-Star parser
31    pub fn new(context_hash: u64) -> Self {
32        Self {
33            context_hash,
34            last_terms: None,
35        }
36    }
37
38    /// Session default graph hash (used when a line omits an explicit graph).
39    pub fn session_context(&self) -> u64 {
40        self.context_hash
41    }
42
43    /// Term strings from the last successfully parsed line.
44    pub fn last_line_terms(&self) -> Option<&NQuadsLineTerms> {
45        self.last_terms.as_ref()
46    }
47
48    fn graph_hash_from_token(&self, graph: &str) -> u64 {
49        if graph.is_empty() {
50            self.context_hash
51        } else {
52            generate_60bit_token(graph.as_bytes())
53        }
54    }
55
56    fn store_regular_terms(
57        &mut self,
58        subject_str: &str,
59        predicate_str: &str,
60        object_str: &str,
61        graph_str: &str,
62    ) {
63        self.last_terms = Some(NQuadsLineTerms {
64            subject: subject_str.to_string(),
65            predicate: predicate_str.to_string(),
66            object: object_str.to_string(),
67            graph: graph_str.to_string(),
68            ..Default::default()
69        });
70    }
71
72    fn store_embedded_terms(
73        &mut self,
74        outer_predicate_str: &str,
75        outer_object_str: &str,
76        outer_graph_str: &str,
77    ) {
78        self.last_terms = Some(NQuadsLineTerms {
79            outer_predicate: outer_predicate_str.to_string(),
80            outer_object: outer_object_str.to_string(),
81            outer_graph: outer_graph_str.to_string(),
82            ..Default::default()
83        });
84    }
85
86    /// Parse an N-Quads line (subject, predicate, object, graph)
87    fn parse_line(&mut self, line: &str) -> Result<ParseResult, RdfStarParseError> {
88        let line = line.trim();
89        if line.is_empty() || line.starts_with('#') {
90            return Ok(ParseResult::Comment);
91        }
92
93        // Check for embedded triple start marker
94        if line.starts_with("<<<") {
95            self.parse_embedded_triple_line(line)
96        } else {
97            self.parse_quad_line(line)
98        }
99    }
100
101    /// Parse a regular N-Quads quad
102    fn parse_quad_line(&mut self, line: &str) -> Result<ParseResult, RdfStarParseError> {
103        // Format: <subject> <predicate> <object> <graph> .
104        let parts: Vec<&str> = line.split_whitespace().collect();
105        if parts.len() < 5 {
106            return Err(RdfStarParseError::InvalidSyntax);
107        }
108
109        let subject_str = parts[0];
110        let predicate_str = parts[1];
111        let object_str = parts[2];
112        let graph_str = parts[3];
113
114        // Strip angle brackets
115        let subject = subject_str.trim_start_matches('<').trim_end_matches('>');
116        let predicate = predicate_str.trim_start_matches('<').trim_end_matches('>');
117        let object = object_str.trim_start_matches('<').trim_end_matches('>');
118        let graph = graph_str.trim_start_matches('<').trim_end_matches('>');
119
120        let subject_hash = generate_60bit_token(subject.as_bytes());
121        let predicate_hash = generate_60bit_token(predicate.as_bytes());
122        let object_hash = generate_60bit_token(object.as_bytes());
123        let graph_hash = self.graph_hash_from_token(graph);
124        self.store_regular_terms(subject, predicate, object, graph);
125
126        Ok(ParseResult::RegularQuad {
127            subject: subject_hash,
128            predicate: predicate_hash,
129            object: object_hash,
130            graph: graph_hash,
131        })
132    }
133
134    /// Parse an embedded triple line with graph context
135    fn parse_embedded_triple_line(&mut self, line: &str) -> Result<ParseResult, RdfStarParseError> {
136        // Format: <<<subject> <predicate> <object>>> <predicate> <object> <graph> .
137
138        // Find the closing >>> for the embedded triple
139        let end_embedded = line
140            .find(">>>")
141            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
142
143        // Extract the embedded triple part
144        let embedded_part = &line[3..end_embedded]; // Skip <<<
145
146        // Parse the embedded triple components
147        let embedded_parts: Vec<&str> = embedded_part.split_whitespace().collect();
148        if embedded_parts.len() < 3 {
149            return Err(RdfStarParseError::MalformedEmbeddedTriple);
150        }
151
152        let subject = embedded_parts[0]
153            .trim_start_matches('<')
154            .trim_end_matches('>');
155        let predicate = embedded_parts[1]
156            .trim_start_matches('<')
157            .trim_end_matches('>');
158        let object = embedded_parts[2]
159            .trim_start_matches('<')
160            .trim_end_matches('>');
161
162        let subject_hash = generate_60bit_token(subject.as_bytes());
163        let predicate_hash = generate_60bit_token(predicate.as_bytes());
164        let object_hash = generate_60bit_token(object.as_bytes());
165
166        // Generate Virtual ID for the embedded triple
167        let virtual_id = generate_embedded_triple_id(subject_hash, predicate_hash, object_hash);
168
169        // Parse the outer triple (the part after >>>)
170        let remaining = &line[end_embedded + 3..]; // Skip >>>
171        let outer_parts: Vec<&str> = remaining.split_whitespace().collect();
172        if outer_parts.len() < 3 {
173            return Err(RdfStarParseError::MalformedEmbeddedTriple);
174        }
175
176        let outer_predicate = outer_parts[0].trim_start_matches('<').trim_end_matches('>');
177        let outer_object = outer_parts[1].trim_start_matches('<').trim_end_matches('>');
178        let outer_graph = outer_parts[2].trim_start_matches('<').trim_end_matches('>');
179
180        let outer_predicate_hash = generate_60bit_token(outer_predicate.as_bytes());
181        let outer_object_hash = generate_60bit_token(outer_object.as_bytes());
182        let outer_graph_hash = self.graph_hash_from_token(outer_graph);
183        self.store_embedded_terms(outer_predicate, outer_object, outer_graph);
184
185        Ok(ParseResult::EmbeddedQuad {
186            virtual_id,
187            components: [subject_hash, predicate_hash, object_hash],
188            outer_predicate: outer_predicate_hash,
189            outer_object: outer_object_hash,
190            outer_graph: outer_graph_hash,
191        })
192    }
193}
194
195impl RdfStarParser for NQuadsStarParser {
196    fn parse_embedded_triple(
197        &mut self,
198        input: &[u8],
199    ) -> Result<(u64, [u64; 3]), RdfStarParseError> {
200        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
201
202        match self.parse_line(line)? {
203            ParseResult::EmbeddedQuad {
204                virtual_id,
205                components,
206                ..
207            } => Ok((virtual_id, components)),
208            _ => Err(RdfStarParseError::MalformedEmbeddedTriple),
209        }
210    }
211
212    fn parse_triple(&mut self, input: &[u8]) -> Result<(u64, u64, u64), RdfStarParseError> {
213        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
214
215        match self.parse_line(line)? {
216            ParseResult::RegularQuad {
217                subject,
218                predicate,
219                object,
220                ..
221            } => Ok((subject, predicate, object)),
222            _ => Err(RdfStarParseError::InvalidSyntax),
223        }
224    }
225
226    fn parse_quad(&mut self, input: &[u8]) -> Result<(u64, u64, u64, u64), RdfStarParseError> {
227        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
228
229        match self.parse_line(line)? {
230            ParseResult::RegularQuad {
231                subject,
232                predicate,
233                object,
234                graph,
235                ..
236            } => Ok((subject, predicate, object, graph)),
237            ParseResult::EmbeddedQuad {
238                outer_predicate,
239                outer_object,
240                outer_graph,
241                ..
242            } => Ok((0, outer_predicate, outer_object, outer_graph)),
243            _ => Err(RdfStarParseError::InvalidSyntax),
244        }
245    }
246
247    fn supports_quads(&self) -> bool {
248        true
249    }
250
251    fn supports_named_graphs(&self) -> bool {
252        true
253    }
254
255    fn format_name(&self) -> &'static str {
256        "N-Quads-Star"
257    }
258}
259
260/// Parse result for N-Quads-Star
261enum ParseResult {
262    Comment,
263    RegularQuad {
264        subject: u64,
265        predicate: u64,
266        object: u64,
267        graph: u64,
268    },
269    EmbeddedQuad {
270        virtual_id: u64,
271        components: [u64; 3],
272        outer_predicate: u64,
273        outer_object: u64,
274        outer_graph: u64,
275    },
276}
277
278/// Parse N-Quads-Star into any [`QuinSink`].
279pub fn parse_nquads_star_into<R: std::io::Read, S: crate::sparql_library::quin_sink::QuinSink>(
280    reader: R,
281    context_hash: u64,
282    sink: &mut S,
283) -> Result<u64, Box<dyn std::error::Error>> {
284    use std::io::BufRead;
285
286    let mut parser = NQuadsStarParser::new(context_hash);
287    let mut count = 0;
288    let buf_reader = BufReader::new(reader);
289
290    for line in buf_reader.lines() {
291        let line = line?;
292        match parser.parse_line(&line)? {
293            ParseResult::Comment => continue,
294            ParseResult::RegularQuad {
295                subject,
296                predicate,
297                object,
298                graph,
299                ..
300            } => {
301                sink.push(NQuin {
302                    subject,
303                    predicate,
304                    object,
305                    context: graph, // Use graph as context in NQuin
306                    metadata: 0b10 << 61,
307                    parity: 0,
308                })?;
309                count += 1;
310            }
311            ParseResult::EmbeddedQuad {
312                virtual_id,
313                components,
314                outer_predicate,
315                outer_object,
316                outer_graph,
317                ..
318            } => {
319                sink.push(NQuin {
320                    subject: virtual_id,
321                    predicate: outer_predicate,
322                    object: outer_object,
323                    context: outer_graph,
324                    metadata: 0b10 << 61,
325                    parity: 0,
326                })?;
327                count += 1;
328
329                sink.push(NQuin {
330                    subject: components[0],
331                    predicate: components[1],
332                    object: components[2],
333                    context: outer_graph,
334                    metadata: 0b10 << 61,
335                    parity: 0,
336                })?;
337                count += 1;
338            }
339        }
340    }
341
342    Ok(count)
343}
344
345/// Parse N-Quads-Star stream via external sort.
346pub fn parse_nquads_star_stream<R: std::io::Read>(
347    reader: R,
348    context_hash: u64,
349    sorter: &mut crate::external_sort::ExternalSorter,
350) -> Result<u64, Box<dyn std::error::Error>> {
351    parse_nquads_star_into(reader, context_hash, sorter)
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use crate::rdf_star::RdfStarParser;
358
359    #[test]
360    fn test_nquads_star_parser_creation() {
361        let parser = NQuadsStarParser::new(0);
362        assert_eq!(parser.format_name(), "N-Quads-Star");
363        assert!(parser.supports_quads());
364        assert!(parser.supports_named_graphs());
365    }
366
367    #[test]
368    fn test_parse_regular_quad() {
369        let mut parser = NQuadsStarParser::new(0);
370        let input = b"<http://example.org/Alice> <http://example.org/knows> <http://example.org/Bob> <http://example.org/Graph1> .";
371        let result = parser.parse_quad(input);
372        assert!(result.is_ok());
373        let (s, p, o, g) = result.unwrap();
374        assert_ne!(s, 0);
375        assert_ne!(p, 0);
376        assert_ne!(o, 0);
377        assert_ne!(g, 0);
378    }
379
380    #[test]
381    fn test_parse_embedded_quad() {
382        let mut parser = NQuadsStarParser::new(0);
383        let input = b"<<<http://example.org/Alice> <http://example.org/knows> <http://example.org/Bob>>> <http://example.org/saidBy> <http://example.org/Charlie> <http://example.org/Graph1> .";
384        let result = parser.parse_embedded_triple(input);
385        assert!(result.is_ok());
386        let (virtual_id, components) = result.unwrap();
387        assert_ne!(virtual_id, 0);
388        assert_ne!(components[0], 0);
389        assert_ne!(components[1], 0);
390        assert_ne!(components[2], 0);
391    }
392}
393use std::io::BufReader;