Skip to main content

qualia_core_db/sparql_library/parsers/
ntriples_star.rs

1use crate::lexicon::{generate_60bit_token, generate_embedded_triple_id};
2use crate::rdf_star::{RdfStarParseError, RdfStarParser};
3///
4/// Implements RDF-Star (SPARQL 1.2) parsing for N-Triples syntax with embedded triples.
5/// N-Triples-Star is a line-based format with strict syntax:
6/// - Regular triple: `<subject> <predicate> <object> .`
7/// - Embedded triple: `<<<subject> <predicate> <object>>> <predicate> <object> .`
8use crate::NQuin;
9/// N-Triples-Star Parser for QualiaDB
10use std::io::BufReader;
11
12/// Human-readable term strings from the most recent successful parse.
13#[derive(Debug, Clone, Default)]
14pub struct NTriplesLineTerms {
15    pub subject: String,
16    pub predicate: String,
17    pub object: String,
18    pub outer_predicate: String,
19    pub outer_object: String,
20}
21
22/// N-Triples-Star parser implementation
23pub struct NTriplesStarParser {
24    /// Context hash for the current parsing session
25    context_hash: u64,
26    last_terms: Option<NTriplesLineTerms>,
27}
28
29impl NTriplesStarParser {
30    /// Create a new N-Triples-Star parser
31    pub fn new(context_hash: u64) -> Self {
32        Self {
33            context_hash,
34            last_terms: None,
35        }
36    }
37
38    /// Session context hash stamped onto emitted quads.
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<&NTriplesLineTerms> {
45        self.last_terms.as_ref()
46    }
47
48    /// Parse an N-Triples line (subject, predicate, object)
49    ///
50    /// Handles both regular triples and embedded triples
51    fn parse_line(&mut self, line: &str) -> Result<ParseResult, RdfStarParseError> {
52        let line = line.trim();
53        if line.is_empty() || line.starts_with('#') {
54            return Ok(ParseResult::Comment);
55        }
56
57        // Check for embedded triple start marker
58        if line.starts_with("<<<") {
59            self.parse_embedded_triple_line(line)
60        } else {
61            self.parse_regular_triple_line(line)
62        }
63    }
64
65    /// Parse a regular N-Triples triple
66    fn parse_regular_triple_line(&mut self, line: &str) -> Result<ParseResult, RdfStarParseError> {
67        // Format: <subject> <predicate> <object> .
68        let parts: Vec<&str> = line.split_whitespace().collect();
69        if parts.len() < 4 {
70            return Err(RdfStarParseError::InvalidSyntax);
71        }
72
73        let subject_str = parts[0];
74        let predicate_str = parts[1];
75        let object_str = parts[2];
76
77        // Strip angle brackets
78        let subject = subject_str.trim_start_matches('<').trim_end_matches('>');
79        let predicate = predicate_str.trim_start_matches('<').trim_end_matches('>');
80        let object = object_str.trim_start_matches('<').trim_end_matches('>');
81
82        let subject_hash = generate_60bit_token(subject.as_bytes());
83        let predicate_hash = generate_60bit_token(predicate.as_bytes());
84        let object_hash = generate_60bit_token(object.as_bytes());
85        self.last_terms = Some(NTriplesLineTerms {
86            subject: subject.to_string(),
87            predicate: predicate.to_string(),
88            object: object.to_string(),
89            ..Default::default()
90        });
91
92        Ok(ParseResult::RegularTriple {
93            subject: subject_hash,
94            predicate: predicate_hash,
95            object: object_hash,
96        })
97    }
98
99    /// Parse an embedded triple line
100    fn parse_embedded_triple_line(&mut self, line: &str) -> Result<ParseResult, RdfStarParseError> {
101        // Format: <<<subject> <predicate> <object>>> <predicate> <object> .
102        // This is complex - need to find the closing >>> and then parse the outer triple
103
104        // Find the closing >>> for the embedded triple
105        let end_embedded = line
106            .find(">>>")
107            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
108
109        // Extract the embedded triple part
110        let embedded_part = &line[3..end_embedded]; // Skip <<<
111
112        // Parse the embedded triple components
113        let embedded_parts: Vec<&str> = embedded_part.split_whitespace().collect();
114        if embedded_parts.len() < 3 {
115            return Err(RdfStarParseError::MalformedEmbeddedTriple);
116        }
117
118        let subject = embedded_parts[0]
119            .trim_start_matches('<')
120            .trim_end_matches('>');
121        let predicate = embedded_parts[1]
122            .trim_start_matches('<')
123            .trim_end_matches('>');
124        let object = embedded_parts[2]
125            .trim_start_matches('<')
126            .trim_end_matches('>');
127
128        let subject_hash = generate_60bit_token(subject.as_bytes());
129        let predicate_hash = generate_60bit_token(predicate.as_bytes());
130        let object_hash = generate_60bit_token(object.as_bytes());
131
132        // Generate Virtual ID for the embedded triple
133        let virtual_id = generate_embedded_triple_id(subject_hash, predicate_hash, object_hash);
134
135        // Parse the outer triple (the part after >>>)
136        let remaining = &line[end_embedded + 3..]; // Skip >>>
137        let outer_parts: Vec<&str> = remaining.split_whitespace().collect();
138        if outer_parts.len() < 2 {
139            return Err(RdfStarParseError::MalformedEmbeddedTriple);
140        }
141
142        let outer_predicate = outer_parts[0].trim_start_matches('<').trim_end_matches('>');
143        let outer_object = outer_parts[1].trim_start_matches('<').trim_end_matches('>');
144
145        let outer_predicate_hash = generate_60bit_token(outer_predicate.as_bytes());
146        let outer_object_hash = generate_60bit_token(outer_object.as_bytes());
147        self.last_terms = Some(NTriplesLineTerms {
148            outer_predicate: outer_predicate.to_string(),
149            outer_object: outer_object.to_string(),
150            ..Default::default()
151        });
152
153        Ok(ParseResult::EmbeddedTriple {
154            virtual_id,
155            components: [subject_hash, predicate_hash, object_hash],
156            outer_predicate: outer_predicate_hash,
157            outer_object: outer_object_hash,
158        })
159    }
160}
161
162impl RdfStarParser for NTriplesStarParser {
163    fn parse_embedded_triple(
164        &mut self,
165        input: &[u8],
166    ) -> Result<(u64, [u64; 3]), RdfStarParseError> {
167        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
168
169        match self.parse_line(line)? {
170            ParseResult::EmbeddedTriple {
171                virtual_id,
172                components,
173                ..
174            } => Ok((virtual_id, components)),
175            _ => Err(RdfStarParseError::MalformedEmbeddedTriple),
176        }
177    }
178
179    fn parse_triple(&mut self, input: &[u8]) -> Result<(u64, u64, u64), RdfStarParseError> {
180        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
181
182        match self.parse_line(line)? {
183            ParseResult::RegularTriple {
184                subject,
185                predicate,
186                object,
187                ..
188            } => Ok((subject, predicate, object)),
189            _ => Err(RdfStarParseError::InvalidSyntax),
190        }
191    }
192
193    fn parse_quad(&mut self, _input: &[u8]) -> Result<(u64, u64, u64, u64), RdfStarParseError> {
194        // N-Triples-Star doesn't support quads natively (use N-Quads-Star for that)
195        Err(RdfStarParseError::UnsupportedFeature)
196    }
197
198    fn supports_quads(&self) -> bool {
199        false
200    }
201
202    fn supports_named_graphs(&self) -> bool {
203        false
204    }
205
206    fn format_name(&self) -> &'static str {
207        "N-Triples-Star"
208    }
209}
210
211/// Parse result for N-Triples-Star
212enum ParseResult {
213    Comment,
214    RegularTriple {
215        subject: u64,
216        predicate: u64,
217        object: u64,
218    },
219    EmbeddedTriple {
220        virtual_id: u64,
221        components: [u64; 3],
222        outer_predicate: u64,
223        outer_object: u64,
224    },
225}
226
227/// Parse N-Triples-Star into any [`QuinSink`] (bounded collector or external sorter).
228pub fn parse_ntriples_star_into<R: std::io::Read, S: crate::sparql_library::quin_sink::QuinSink>(
229    reader: R,
230    context_hash: u64,
231    sink: &mut S,
232) -> Result<u64, Box<dyn std::error::Error>> {
233    use std::io::BufRead;
234
235    let mut parser = NTriplesStarParser::new(context_hash);
236    let mut count = 0;
237    let buf_reader = BufReader::new(reader);
238
239    for line in buf_reader.lines() {
240        let line = line?;
241        match parser.parse_line(&line)? {
242            ParseResult::Comment => continue,
243            ParseResult::RegularTriple {
244                subject,
245                predicate,
246                object,
247                ..
248            } => {
249                sink.push(NQuin {
250                    subject,
251                    predicate,
252                    object,
253                    context: context_hash,
254                    metadata: 0b10 << 61,
255                    parity: 0,
256                })?;
257                count += 1;
258            }
259            ParseResult::EmbeddedTriple {
260                virtual_id,
261                components,
262                outer_predicate,
263                outer_object,
264                ..
265            } => {
266                sink.push(NQuin {
267                    subject: virtual_id,
268                    predicate: outer_predicate,
269                    object: outer_object,
270                    context: context_hash,
271                    metadata: 0b10 << 61,
272                    parity: 0,
273                })?;
274                count += 1;
275
276                sink.push(NQuin {
277                    subject: components[0],
278                    predicate: components[1],
279                    object: components[2],
280                    context: context_hash,
281                    metadata: 0b10 << 61,
282                    parity: 0,
283                })?;
284                count += 1;
285            }
286        }
287    }
288
289    Ok(count)
290}
291
292/// Parse N-Triples-Star stream and emit Quins via external sort.
293pub fn parse_ntriples_star_stream<R: std::io::Read>(
294    reader: R,
295    context_hash: u64,
296    sorter: &mut crate::external_sort::ExternalSorter,
297) -> Result<u64, Box<dyn std::error::Error>> {
298    parse_ntriples_star_into(reader, context_hash, sorter)
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::rdf_star::RdfStarParser;
305
306    #[test]
307    fn test_ntriples_star_parser_creation() {
308        let parser = NTriplesStarParser::new(0);
309        assert_eq!(parser.format_name(), "N-Triples-Star");
310        assert!(!parser.supports_quads());
311        assert!(!parser.supports_named_graphs());
312    }
313
314    #[test]
315    fn test_parse_regular_triple() {
316        let mut parser = NTriplesStarParser::new(0);
317        let input =
318            b"<http://example.org/Alice> <http://example.org/knows> <http://example.org/Bob> .";
319        let result = parser.parse_triple(input);
320        assert!(result.is_ok());
321        let (s, p, o) = result.unwrap();
322        assert_ne!(s, 0);
323        assert_ne!(p, 0);
324        assert_ne!(o, 0);
325    }
326
327    #[test]
328    fn test_parse_embedded_triple() {
329        let mut parser = NTriplesStarParser::new(0);
330        let input = b"<<<http://example.org/Alice> <http://example.org/knows> <http://example.org/Bob>>> <http://example.org/saidBy> <http://example.org/Charlie> .";
331        let result = parser.parse_embedded_triple(input);
332        assert!(result.is_ok());
333        let (virtual_id, components) = result.unwrap();
334        assert_ne!(virtual_id, 0);
335        assert_ne!(components[0], 0);
336        assert_ne!(components[1], 0);
337        assert_ne!(components[2], 0);
338    }
339}