1use crate::{
17 build_pyramid_meta_algo, Dictionary, DictionaryBuilder, GraphIndexBuilder, PyramidAlgo,
18 DEFAULT_TILE_BUDGET,
19};
20
21pub type RawTriple = (String, String, String);
23
24pub type RawQuad = (String, String, String, Option<String>);
26
27#[derive(Debug, thiserror::Error)]
32#[non_exhaustive]
33pub enum IngestError {
34 #[error("line {0}: {1}")]
37 Line(usize, &'static str),
38 #[error("turtle: {0}")]
40 Turtle(String),
41 #[error("rdf/xml: {0}")]
43 RdfXml(String),
44 #[error("unknown input format: {0} (expected nt, nq, ttl, or rdf/xml)")]
46 UnknownFormat(String),
47 #[error("io: {0}")]
49 Io(String),
50}
51
52fn estimate_statements(input: &str) -> usize {
57 bytecount_newlines(input).max(1)
58}
59
60fn bytecount_newlines(input: &str) -> usize {
62 input.as_bytes().iter().filter(|&&b| b == b'\n').count()
63}
64
65fn parse_nq_line(raw: &str, lineno: usize) -> Result<Option<RawQuad>, IngestError> {
68 let line = raw.trim();
69 if line.is_empty() || line.starts_with('#') {
70 return Ok(None);
71 }
72 let stripped = line
73 .strip_suffix('.')
74 .ok_or(IngestError::Line(lineno, "missing trailing '.'"))?
75 .trim_end();
76 let (s, rest) = take_term(stripped).ok_or(IngestError::Line(lineno, "bad subject"))?;
77 let (p, rest) =
78 take_term(rest.trim_start()).ok_or(IngestError::Line(lineno, "bad predicate"))?;
79 let (o, rest) = take_term(rest.trim_start()).ok_or(IngestError::Line(lineno, "bad object"))?;
80 let rest = rest.trim();
81 let graph = if rest.is_empty() {
82 None
83 } else {
84 let (g, tail) = take_term(rest).ok_or(IngestError::Line(lineno, "bad graph"))?;
85 if !tail.trim().is_empty() {
86 return Err(IngestError::Line(lineno, "trailing content after graph"));
87 }
88 Some(g)
89 };
90 Ok(Some((s, p, o, graph)))
91}
92
93fn parse_nt_line(raw: &str, lineno: usize) -> Result<Option<RawTriple>, IngestError> {
95 let line = raw.trim();
96 if line.is_empty() || line.starts_with('#') {
97 return Ok(None);
98 }
99 let stripped = line
100 .strip_suffix('.')
101 .ok_or(IngestError::Line(lineno, "missing trailing '.'"))?
102 .trim_end();
103 let (s, rest) = take_term(stripped).ok_or(IngestError::Line(lineno, "bad subject"))?;
104 let (p, rest) =
105 take_term(rest.trim_start()).ok_or(IngestError::Line(lineno, "bad predicate"))?;
106 let (o, rest) = take_term(rest.trim_start()).ok_or(IngestError::Line(lineno, "bad object"))?;
107 if !rest.trim().is_empty() {
108 return Err(IngestError::Line(lineno, "trailing content after object"));
109 }
110 Ok(Some((s, p, o)))
111}
112
113pub fn parse_quads(input: &str) -> Result<Vec<RawQuad>, IngestError> {
115 let mut out = Vec::with_capacity(estimate_statements(input));
116 for (i, raw) in input.lines().enumerate() {
117 if let Some(q) = parse_nq_line(raw, i + 1)? {
118 out.push(q);
119 }
120 }
121 Ok(out)
122}
123
124pub fn parse(input: &str) -> Result<Vec<RawTriple>, IngestError> {
126 let mut out = Vec::with_capacity(estimate_statements(input));
127 for (i, raw) in input.lines().enumerate() {
128 if let Some(t) = parse_nt_line(raw, i + 1)? {
129 out.push(t);
130 }
131 }
132 Ok(out)
133}
134
135pub fn parse_reader<R: std::io::BufRead>(
143 reader: R,
144 format: &str,
145 cap: usize,
146) -> Result<Vec<RawQuad>, IngestError> {
147 let mut out = Vec::with_capacity(cap);
148 stream_reader(reader, format, &mut |q| out.push(q))?;
149 Ok(out)
150}
151
152pub fn stream_reader<R: std::io::BufRead>(
161 reader: R,
162 format: &str,
163 f: &mut dyn FnMut(RawQuad),
164) -> Result<(), IngestError> {
165 if format != "nt" && format != "nq" {
166 return Err(IngestError::UnknownFormat(format.to_string()));
167 }
168 for (i, line) in reader.lines().enumerate() {
169 let line = line.map_err(|e| IngestError::Io(e.to_string()))?;
170 if format == "nq" {
171 if let Some(q) = parse_nq_line(&line, i + 1)? {
172 f(q);
173 }
174 } else if let Some((s, p, o)) = parse_nt_line(&line, i + 1)? {
175 f((s, p, o, None));
176 }
177 }
178 Ok(())
179}
180
181pub fn parse_turtle(text: &str) -> Result<Vec<RawTriple>, IngestError> {
183 let mut out = Vec::new();
184 for r in oxttl::TurtleParser::new()
188 .with_quoted_triples()
189 .for_reader(text.as_bytes())
190 {
191 let t = r.map_err(|e| IngestError::Turtle(e.to_string()))?;
192 out.push((
193 t.subject.to_string(),
194 t.predicate.to_string(),
195 t.object.to_string(),
196 ));
197 }
198 Ok(out)
199}
200
201pub fn parse_rdfxml(text: &str) -> Result<Vec<RawTriple>, IngestError> {
206 let mut out = Vec::new();
207 for r in oxrdfxml::RdfXmlParser::new().for_reader(text.as_bytes()) {
208 let t = r.map_err(|e| IngestError::RdfXml(e.to_string()))?;
209 out.push((
210 t.subject.to_string(),
211 t.predicate.to_string(),
212 t.object.to_string(),
213 ));
214 }
215 Ok(out)
216}
217
218pub fn parse_statements(text: &str, format: &str) -> Result<Vec<RawQuad>, IngestError> {
221 match format {
222 "nq" => parse_quads(text),
223 "ttl" => Ok(parse_turtle(text)?
224 .into_iter()
225 .map(|(s, p, o)| (s, p, o, None))
226 .collect()),
227 "rdfxml" => Ok(parse_rdfxml(text)?
228 .into_iter()
229 .map(|(s, p, o)| (s, p, o, None))
230 .collect()),
231 "nt" => Ok(parse(text)?
232 .into_iter()
233 .map(|(s, p, o)| (s, p, o, None))
234 .collect()),
235 other => Err(IngestError::UnknownFormat(other.to_string())),
236 }
237}
238
239pub(crate) fn take_term(s: &str) -> Option<(String, &str)> {
241 let bytes = s.as_bytes();
242 let first = *bytes.first()?;
243 match first {
244 b'<' if bytes.get(1) == Some(&b'<') => {
255 let (inner, rdf12) = match s[2..].strip_prefix('(') {
257 Some(after) => (after.trim_start(), true),
258 None => (s[2..].trim_start(), false),
259 };
260 let (subj, r) = take_term(inner)?;
261 let (pred, r) = take_term(r.trim_start())?;
262 let (obj, r) = take_term(r.trim_start())?;
263 let r = r.trim_start();
264 let rest = if rdf12 {
265 r.strip_prefix(")>>")?
266 } else {
267 r.strip_prefix(">>")?
268 };
269 Some((format!("<<{subj} {pred} {obj}>>"), rest))
274 }
275 b'<' => {
276 let end = s.find('>')?;
278 Some((s[..=end].to_string(), &s[end + 1..]))
279 }
280 b'_' => {
281 let end = s.find(char::is_whitespace).unwrap_or(s.len());
283 Some((s[..end].to_string(), &s[end..]))
284 }
285 b'"' => {
286 let mut i = 1;
288 let b = s.as_bytes();
289 while i < b.len() {
290 match b[i] {
291 b'\\' => i += 2, b'"' => break,
293 _ => i += 1,
294 }
295 }
296 if i >= b.len() {
297 return None; }
299 let mut end = i + 1; if s[end..].starts_with("^^<") {
301 let close = s[end..].find('>')? + end;
302 end = close + 1;
303 } else if s[end..].starts_with('@') {
304 let mut j = end + 1; while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'-') {
311 j += 1;
312 }
313 end = j;
314 }
315 Some((s[..end].to_string(), &s[end..]))
316 }
317 _ => None,
318 }
319}
320
321pub(crate) fn quoted_triple_parts(t: &str) -> Option<(String, String, String)> {
327 let inner = t.strip_prefix("<<")?.strip_suffix(">>")?.trim();
328 let (s, r) = take_term(inner)?;
329 let (p, r) = take_term(r.trim_start())?;
330 let (o, r) = take_term(r.trim_start())?;
331 if !r.trim().is_empty() {
332 return None;
333 }
334 Some((s, p, o))
335}
336
337#[derive(Debug, Clone, Copy)]
339pub struct BuildStats {
340 pub statements: usize,
342 pub default_triples: usize,
344 pub named_graphs: usize,
346 pub terms: usize,
348 pub pyramid_levels: u16,
350}
351
352pub fn assemble_dataset(quads: Vec<RawQuad>, metadata: &[u8]) -> (Vec<u8>, BuildStats) {
358 let blob = metadata.to_vec();
359 assemble_dataset_with(quads, move |_, _| blob)
360}
361
362pub fn assemble_dataset_with(
367 quads: Vec<RawQuad>,
368 metadata: impl FnOnce(&BuildStats, &[RawQuad]) -> Vec<u8>,
369) -> (Vec<u8>, BuildStats) {
370 assemble_dataset_with_opts(quads, true, false, None, metadata)
371}
372
373pub fn assemble_dataset_with_opts(
380 quads: Vec<RawQuad>,
381 with_pyramid: bool,
382 with_text_index: bool,
383 type_override: Option<&str>,
384 metadata: impl FnOnce(&BuildStats, &[RawQuad]) -> Vec<u8>,
385) -> (Vec<u8>, BuildStats) {
386 assemble_dataset_with_opts_algo(
387 quads,
388 with_pyramid,
389 with_text_index,
390 type_override,
391 PyramidAlgo::Louvain,
392 metadata,
393 )
394}
395
396#[allow(clippy::too_many_arguments)]
399pub fn assemble_dataset_with_opts_algo(
400 quads: Vec<RawQuad>,
401 with_pyramid: bool,
402 with_text_index: bool,
403 type_override: Option<&str>,
404 algo: PyramidAlgo,
405 metadata: impl FnOnce(&BuildStats, &[RawQuad]) -> Vec<u8>,
406) -> (Vec<u8>, BuildStats) {
407 use std::collections::BTreeMap;
408
409 let mut db = DictionaryBuilder::new();
410 for (s, p, o, _) in &quads {
411 db.observe(s, p, o);
412 }
413 let dict = db.build();
414
415 let mut default_triples = Vec::new();
416 let mut named: BTreeMap<String, Vec<(u32, u32, u32)>> = BTreeMap::new();
417 for (s, p, o, g) in &quads {
418 let t = dict.encode(s, p, o).expect("observed term");
419 match g {
420 None => default_triples.push(t),
421 Some(graph) => named.entry(graph.clone()).or_default().push(t),
422 }
423 }
424
425 let stats = BuildStats {
434 statements: quads.len(),
435 default_triples: default_triples.len(),
436 named_graphs: named.len(),
437 terms: dict.term_count() as usize,
438 pyramid_levels: 0,
439 };
440 let blob = metadata(&stats, &quads);
441 drop(quads);
442
443 finish_assembly(
444 dict,
445 default_triples,
446 named,
447 with_pyramid,
448 with_text_index,
449 type_override,
450 algo,
451 blob,
452 stats,
453 )
454}
455
456pub fn assemble_dataset_streaming<S>(
471 stream: S,
472 with_pyramid: bool,
473 with_text_index: bool,
474 type_override: Option<&str>,
475 metadata: impl FnOnce(&BuildStats, &Dictionary, &[(u32, u32, u32)]) -> Vec<u8>,
476) -> Result<(Vec<u8>, BuildStats), IngestError>
477where
478 S: FnMut(&mut dyn FnMut(RawQuad)) -> Result<(), IngestError>,
479{
480 assemble_dataset_streaming_algo(
481 stream,
482 with_pyramid,
483 with_text_index,
484 type_override,
485 PyramidAlgo::Louvain,
486 metadata,
487 )
488}
489
490#[allow(clippy::too_many_arguments)]
493pub fn assemble_dataset_streaming_algo<S>(
494 mut stream: S,
495 with_pyramid: bool,
496 with_text_index: bool,
497 type_override: Option<&str>,
498 algo: PyramidAlgo,
499 metadata: impl FnOnce(&BuildStats, &Dictionary, &[(u32, u32, u32)]) -> Vec<u8>,
500) -> Result<(Vec<u8>, BuildStats), IngestError>
501where
502 S: FnMut(&mut dyn FnMut(RawQuad)) -> Result<(), IngestError>,
503{
504 use std::collections::BTreeMap;
505
506 let mut db = DictionaryBuilder::new();
509 stream(&mut |(s, p, o, _g)| db.observe(&s, &p, &o))?;
510 let dict = db.build();
511
512 let mut default_triples: Vec<(u32, u32, u32)> = Vec::new();
514 let mut named: BTreeMap<String, Vec<(u32, u32, u32)>> = BTreeMap::new();
515 stream(&mut |(s, p, o, g)| {
516 let t = dict.encode(&s, &p, &o).expect("observed term");
517 match g {
518 None => default_triples.push(t),
519 Some(graph) => named.entry(graph).or_default().push(t),
520 }
521 })?;
522
523 let statements = default_triples.len() + named.values().map(Vec::len).sum::<usize>();
524 let stats = BuildStats {
525 statements,
526 default_triples: default_triples.len(),
527 named_graphs: named.len(),
528 terms: dict.term_count() as usize,
529 pyramid_levels: 0,
530 };
531 let blob = metadata(&stats, &dict, &default_triples);
532 Ok(finish_assembly(
533 dict,
534 default_triples,
535 named,
536 with_pyramid,
537 with_text_index,
538 type_override,
539 algo,
540 blob,
541 stats,
542 ))
543}
544
545#[allow(clippy::too_many_arguments)]
550fn finish_assembly(
551 dict: Dictionary,
552 default_triples: Vec<(u32, u32, u32)>,
553 named: std::collections::BTreeMap<String, Vec<(u32, u32, u32)>>,
554 with_pyramid: bool,
555 with_text_index: bool,
556 type_override: Option<&str>,
557 algo: PyramidAlgo,
558 blob: Vec<u8>,
559 mut stats: BuildStats,
560) -> (Vec<u8>, BuildStats) {
561 let has_named = !named.is_empty();
562
563 let (meta, levels) = if with_pyramid {
566 build_pyramid_meta_algo(
567 &dict,
568 &default_triples,
569 DEFAULT_TILE_BUDGET,
570 type_override,
571 algo,
572 )
573 } else {
574 (Vec::new(), 0)
575 };
576 stats.pyramid_levels = levels;
577
578 let text_index = if with_text_index {
579 crate::file::compute_text_index(&dict, &default_triples)
580 } else {
581 Vec::new()
582 };
583
584 let codec = crate::file::writer_codec();
591 let dict_container = crate::file::encode_dict_container(&dict, codec);
592 let term_count = dict.term_count() as u64;
593 let has_quoted_triples = dict.has_quoted_triples();
594 drop(dict);
595
596 let build_index = |triples: Vec<(u32, u32, u32)>| -> crate::GraphIndex {
602 let n = triples.len();
603 let b = GraphIndexBuilder::from_triples(triples);
604 if n > LOWMEM_TRIPLE_THRESHOLD {
605 b.build_seq()
606 } else {
607 b.build()
608 }
609 };
610 let def = build_index(default_triples);
611 let named_indexes: Vec<(String, crate::GraphIndex)> = named
612 .into_iter()
613 .map(|(g, ts)| (g, build_index(ts)))
614 .collect();
615
616 let bytes = crate::file::write_dataset_from_parts(
617 &dict_container,
618 term_count,
619 &def,
620 &named_indexes,
621 has_named,
622 has_quoted_triples,
623 &meta,
624 levels,
625 &blob,
626 &text_index,
627 codec,
628 );
629 (bytes, stats)
630}
631
632const LOWMEM_TRIPLE_THRESHOLD: usize = 30_000_000;
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use crate::Rete;
642
643 #[test]
644 fn parses_iris_bnodes_literals() {
645 let input = r#"
646 # a comment
647 <http://ex/Alice> <http://ex/knows> <http://ex/Bob> .
648 <http://ex/Alice> <http://ex/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
649 <http://ex/Bob> <http://ex/label> "Bob"@en .
650 _:b0 <http://ex/p> "plain" .
651 "#;
652 let t = parse(input).unwrap();
653 assert_eq!(t.len(), 4);
654 assert_eq!(t[0].0, "<http://ex/Alice>");
655 assert_eq!(t[0].2, "<http://ex/Bob>");
656 assert_eq!(t[1].2, "\"30\"^^<http://www.w3.org/2001/XMLSchema#integer>");
657 assert_eq!(t[2].2, "\"Bob\"@en");
658 assert_eq!(t[3].0, "_:b0");
659 assert_eq!(t[3].2, "\"plain\"");
660 }
661
662 #[test]
663 fn literal_with_spaces_and_escaped_quote() {
664 let input = r#"<http://ex/s> <http://ex/p> "a \"quoted\" phrase here" ."#;
665 let t = parse(input).unwrap();
666 assert_eq!(t.len(), 1);
667 assert_eq!(t[0].2, r#""a \"quoted\" phrase here""#);
668 }
669
670 #[test]
675 fn quoted_triple_langtagged_object() {
676 let s = r#"<<<http://ex/sp> <http://ex/name> "Hirondelle rustique"@fr>>"#;
677 let (tok, rest) = take_term(s).unwrap();
678 assert_eq!(
679 tok,
680 r#"<<<http://ex/sp> <http://ex/name> "Hirondelle rustique"@fr>>"#
681 );
682 assert_eq!(rest, "");
683 let line = r#"<<<http://ex/sp> <http://ex/name> "Oreneta vulgar"@ca>> <http://purl.org/dc/terms/source> "Catalogue of Life" ."#;
685 let t = parse(line).unwrap();
686 assert_eq!(t.len(), 1);
687 assert_eq!(
688 t[0].0,
689 r#"<<<http://ex/sp> <http://ex/name> "Oreneta vulgar"@ca>>"#
690 );
691 assert_eq!(t[0].2, r#""Catalogue of Life""#);
692 assert_eq!(take_term(r#""x"@pt-BR ."#).unwrap().0, r#""x"@pt-BR"#);
694 }
695
696 #[test]
700 fn rdf12_triple_term_same_token_as_rdf_star() {
701 let rdf12 = r#"<http://ex/r> <http://ex/p> <<( <http://ex/s> <http://ex/q> "o" )>> ."#;
702 let star = r#"<http://ex/r> <http://ex/p> << <http://ex/s> <http://ex/q> "o" >> ."#;
703 let a = parse(rdf12).unwrap();
704 let b = parse(star).unwrap();
705 assert_eq!(a.len(), 1);
706 assert_eq!(a[0].2, r#"<<<http://ex/s> <http://ex/q> "o">>"#);
707 assert_eq!(
708 a[0].2, b[0].2,
709 "RDF 1.2 and RDF-star must dedupe to one token"
710 );
711 let (tok, rest) =
713 take_term(r#"<<( <http://ex/s> <http://ex/q> <http://ex/o> )>>"#).unwrap();
714 assert_eq!(tok, "<<<http://ex/s> <http://ex/q> <http://ex/o>>>");
715 assert_eq!(rest, "");
716 }
717
718 #[test]
719 fn rejects_missing_dot() {
720 assert!(parse("<a> <b> <c>").is_err());
721 }
722
723 #[test]
724 fn parses_quads_with_and_without_graph() {
725 let input = "<http://ex/a> <http://ex/p> <http://ex/b> .\n\
726 <http://ex/a> <http://ex/p> <http://ex/c> <http://ex/g> .";
727 let q = parse_quads(input).unwrap();
728 assert_eq!(q.len(), 2);
729 assert_eq!(q[0].3, None); assert_eq!(q[1].3, Some("<http://ex/g>".to_string()));
731 }
732
733 #[test]
734 fn parse_reader_matches_text_parse() {
735 let nt = "<http://ex/a> <http://ex/p> <http://ex/b> .\r\n\
738 # comment\n\
739 \n\
740 _:b0 <http://ex/q> \"lit\"@en .\n";
741 let via_text = parse_statements(nt, "nt").unwrap();
742 let via_reader = parse_reader(std::io::Cursor::new(nt), "nt", 0).unwrap();
743 assert_eq!(via_text, via_reader);
744
745 let nq = "<http://ex/a> <http://ex/p> <http://ex/b> .\n\
746 <http://ex/a> <http://ex/p> <http://ex/c> <http://ex/g> .\n";
747 assert_eq!(
748 parse_quads(nq).unwrap(),
749 parse_reader(std::io::Cursor::new(nq), "nq", 0).unwrap()
750 );
751 assert!(parse_reader(std::io::Cursor::new(nt), "ttl", 0).is_err());
753 }
754
755 #[test]
756 fn parse_statements_dispatches_by_format() {
757 let nt = "<http://ex/a> <http://ex/p> <http://ex/b> .";
758 assert_eq!(parse_statements(nt, "nt").unwrap().len(), 1);
759 let ttl = "@prefix ex: <http://ex/> .\nex:A ex:knows ex:B , ex:C .";
760 assert_eq!(parse_statements(ttl, "ttl").unwrap().len(), 2);
761 assert!(parse_statements(nt, "trig").is_err());
762 }
763
764 #[test]
767 fn parses_rdfxml_owl() {
768 let xml = r#"<?xml version="1.0"?>
769 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
770 xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
771 xmlns:owl="http://www.w3.org/2002/07/owl#">
772 <owl:Class rdf:about="http://ex/Dog">
773 <rdfs:subClassOf rdf:resource="http://ex/Animal"/>
774 <rdfs:label>Dog</rdfs:label>
775 </owl:Class>
776 </rdf:RDF>"#;
777 let triples = parse_rdfxml(xml).unwrap();
778 assert_eq!(triples.len(), 3);
780 assert!(triples.iter().any(|(s, p, o)| s == "<http://ex/Dog>"
781 && p == "<http://www.w3.org/2000/01/rdf-schema#subClassOf>"
782 && o == "<http://ex/Animal>"));
783 assert_eq!(parse_statements(xml, "rdfxml").unwrap().len(), 3);
785 assert!(matches!(
787 parse_statements("<rdf:RDF><not closed", "rdfxml"),
788 Err(IngestError::RdfXml(_))
789 ));
790 }
791
792 #[test]
796 fn rdfxml_namespace_fanout_is_bounded() {
797 let declarations = (0..2_000)
798 .map(|i| format!(r#" xmlns:p{i}="http://example.test/{i}/""#))
799 .collect::<String>();
800 let xml = format!(
801 r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"{declarations}/>"#
802 );
803 let result = parse_statements(&xml, "rdfxml");
804 assert!(result.is_ok() || result.unwrap_err().to_string().contains("namespace"));
805 }
806
807 #[test]
811 fn rdfxml_attribute_fanout_completes() {
812 let attributes = (0..2_000)
813 .map(|i| format!(r#" p:a{i}="{i}""#))
814 .collect::<String>();
815 let xml = format!(
816 r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:p="http://example.test/"><rdf:Description rdf:about="http://example.test/s"{attributes}/></rdf:RDF>"#
817 );
818 assert!(parse_statements(&xml, "rdfxml").is_ok());
819 }
820
821 #[test]
824 fn assemble_dataset_round_trips() {
825 let text = "<http://ex/a> <http://ex/knows> <http://ex/b> .\n\
826 <http://ex/b> <http://ex/knows> <http://ex/c> .\n\
827 <http://ex/a> <http://ex/age> \"30\"^^<http://www.w3.org/2001/XMLSchema#integer> .\n\
828 <http://ex/a> <http://ex/p> <http://ex/d> <http://ex/g1> .";
829 let quads = parse_statements(text, "nq").unwrap();
830 let (bytes, stats) = assemble_dataset(quads, &[]);
831 assert_eq!(stats.statements, 4);
832 assert_eq!(stats.default_triples, 3);
833 assert_eq!(stats.named_graphs, 1);
834 assert!(stats.terms >= 7);
835
836 let rete = Rete::open(&bytes).unwrap();
837 assert_eq!(
838 rete.query(None, Some("<http://ex/knows>"), None).len(),
839 2,
840 "default-graph pattern query"
841 );
842 assert_eq!(rete.graph_names(), vec!["<http://ex/g1>"]);
843 let out = crate::eval_query(
844 &rete,
845 "SELECT ?x WHERE { ?x <http://ex/knows> ?y . ?y <http://ex/knows> ?z }",
846 )
847 .unwrap();
848 match out {
849 crate::QueryOutput::Select(_, rows) => assert_eq!(rows.len(), 1),
850 other => panic!("expected select result, got {other:?}"),
851 }
852 }
853
854 #[test]
860 fn assemble_minimal_typeless_graph() {
861 let text = "<http://ex/A> <http://ex/knows> <http://ex/B> .\n\
862 <http://ex/B> <http://ex/knows> <http://ex/C> .\n";
863 let quads = parse_statements(text, "nt").unwrap();
864 let (bytes, stats) = assemble_dataset(quads, &[]);
865 assert_eq!(stats.default_triples, 2);
866 let rete = Rete::open(&bytes).unwrap();
867 assert_eq!(rete.query(None, Some("<http://ex/knows>"), None).len(), 2);
868 }
869}