1use std::cell::RefCell;
20use std::collections::{HashMap, HashSet};
21use std::convert::Infallible;
22use std::rc::Rc;
23
24use crate::path_plan::ReachStep;
25use oxrdf::{Graph, NamedNode, Term};
26use shifty_opt::ClosureKind;
27use spareval::{InternalQuad, QueryableDataset};
28
29pub(crate) const SHAPES_GRAPH_IRI: &str = "urn:x-shacl:shapes-graph";
31
32pub type TermId = u32;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub(crate) enum GraphSel {
39 Default,
41 Named(TermId),
43}
44
45struct TermDictInner {
48 id_to_term: Vec<Term>,
49 term_to_id: HashMap<Term, TermId>,
50}
51
52struct TermDictionary(RefCell<TermDictInner>);
56
57impl TermDictionary {
58 fn new() -> Self {
59 Self(RefCell::new(TermDictInner {
60 id_to_term: Vec::new(),
61 term_to_id: HashMap::new(),
62 }))
63 }
64
65 fn intern(&self, term: Term) -> TermId {
67 let mut inner = self.0.borrow_mut();
68 if let Some(&id) = inner.term_to_id.get(&term) {
69 return id;
70 }
71 let id = inner.id_to_term.len() as TermId;
72 inner.term_to_id.insert(term.clone(), id);
73 inner.id_to_term.push(term);
74 id
75 }
76
77 #[cfg(test)]
78 fn get(&self, term: &Term) -> Option<TermId> {
79 self.0.borrow().term_to_id.get(term).copied()
80 }
81
82 fn externalize(&self, id: TermId) -> Option<Term> {
83 self.0.borrow().id_to_term.get(id as usize).cloned()
84 }
85}
86
87struct TripleIndex {
99 spo: Vec<[TermId; 3]>, pos: Vec<[TermId; 3]>, osp: Vec<[TermId; 3]>, }
103
104impl TripleIndex {
105 fn build(mut triples: Vec<[TermId; 3]>) -> Self {
106 triples.sort_unstable();
107 triples.dedup();
108
109 let mut pos: Vec<[TermId; 3]> = triples.iter().map(|&[s, p, o]| [p, o, s]).collect();
110 pos.sort_unstable();
111
112 let mut osp: Vec<[TermId; 3]> = triples.iter().map(|&[s, p, o]| [o, s, p]).collect();
113 osp.sort_unstable();
114
115 Self {
116 spo: triples,
117 pos,
118 osp,
119 }
120 }
121
122 fn contains(&self, s: TermId, p: TermId, o: TermId) -> bool {
123 self.spo.binary_search(&[s, p, o]).is_ok()
124 }
125
126 fn extend(&mut self, triples: Vec<[TermId; 3]>) {
127 if triples.is_empty() {
128 return;
129 }
130 self.spo.extend(triples.iter().copied());
131 self.spo.sort_unstable();
132 self.spo.dedup();
133
134 self.pos.extend(triples.iter().map(|&[s, p, o]| [p, o, s]));
135 self.pos.sort_unstable();
136 self.pos.dedup();
137
138 self.osp
139 .extend(triples.into_iter().map(|[s, p, o]| [o, s, p]));
140 self.osp.sort_unstable();
141 self.osp.dedup();
142 }
143
144 fn range_s(&self, s: TermId) -> &[[TermId; 3]] {
145 let lo = self.spo.partition_point(|t| t[0] < s);
146 let hi = self.spo.partition_point(|t| t[0] <= s);
147 &self.spo[lo..hi]
148 }
149
150 fn range_sp(&self, s: TermId, p: TermId) -> &[[TermId; 3]] {
151 let lo = self.spo.partition_point(|t| (t[0], t[1]) < (s, p));
152 let hi = self.spo.partition_point(|t| (t[0], t[1]) <= (s, p));
153 &self.spo[lo..hi]
154 }
155
156 fn range_p(&self, p: TermId) -> &[[TermId; 3]] {
157 let lo = self.pos.partition_point(|t| t[0] < p);
158 let hi = self.pos.partition_point(|t| t[0] <= p);
159 &self.pos[lo..hi]
160 }
161
162 fn range_po(&self, p: TermId, o: TermId) -> &[[TermId; 3]] {
163 let lo = self.pos.partition_point(|t| (t[0], t[1]) < (p, o));
164 let hi = self.pos.partition_point(|t| (t[0], t[1]) <= (p, o));
165 &self.pos[lo..hi]
166 }
167
168 fn range_o(&self, o: TermId) -> &[[TermId; 3]] {
169 let lo = self.osp.partition_point(|t| t[0] < o);
170 let hi = self.osp.partition_point(|t| t[0] <= o);
171 &self.osp[lo..hi]
172 }
173}
174
175pub struct DatasetStatistics {
179 pub triple_count: u64,
180 pub distinct_subjects: u64,
181 pub distinct_objects: u64,
182 pub predicate_cardinality: HashMap<TermId, u64>,
184 subjects: HashSet<TermId>,
185 objects: HashSet<TermId>,
186}
187
188impl DatasetStatistics {
189 fn compute(triples: &TripleIndex) -> Self {
190 let triple_count = triples.spo.len() as u64;
191 let subjects: HashSet<TermId> = triples.spo.iter().map(|t| t[0]).collect();
192 let objects: HashSet<TermId> = triples.spo.iter().map(|t| t[2]).collect();
193 let mut predicate_cardinality: HashMap<TermId, u64> = HashMap::new();
194 for &[_, p, _] in &triples.spo {
195 *predicate_cardinality.entry(p).or_insert(0) += 1;
196 }
197
198 Self {
199 triple_count,
200 distinct_subjects: subjects.len() as u64,
201 distinct_objects: objects.len() as u64,
202 predicate_cardinality,
203 subjects,
204 objects,
205 }
206 }
207
208 fn extend(&mut self, triples: &[[TermId; 3]]) {
209 self.triple_count += triples.len() as u64;
210 for &[subject, predicate, object] in triples {
211 self.subjects.insert(subject);
212 self.objects.insert(object);
213 *self.predicate_cardinality.entry(predicate).or_insert(0) += 1;
214 }
215 self.distinct_subjects = self.subjects.len() as u64;
216 self.distinct_objects = self.objects.len() as u64;
217 }
218}
219
220pub struct FrozenIndexedDataset {
226 terms: TermDictionary,
227 default_graph: TripleIndex,
228 named_graphs: HashMap<TermId, Vec<[TermId; 3]>>,
231 reach_cache: RefCell<ReachCache>,
232 pub stats: DatasetStatistics,
233}
234
235const MAX_CACHED_REACH_IDS: usize = 1_000_000;
241
242#[derive(PartialEq, Eq, Hash)]
243struct ReachCacheKey {
244 node: TermId,
245 step: ReachStep,
246 kind: ClosureKind,
247 graph: GraphSel,
248}
249
250#[derive(Default)]
251struct ReachCache {
252 entries: HashMap<ReachCacheKey, Rc<HashSet<TermId>>>,
256 cached_ids: usize,
257}
258
259impl FrozenIndexedDataset {
260 pub fn from_graph(graph: &Graph) -> Self {
262 let terms = TermDictionary::new();
263 let triples = intern_graph(graph, &terms);
264 let default_graph = TripleIndex::build(triples);
265 let stats = DatasetStatistics::compute(&default_graph);
266 Self {
267 terms,
268 default_graph,
269 named_graphs: HashMap::new(),
270 reach_cache: RefCell::new(ReachCache::default()),
271 stats,
272 }
273 }
274
275 pub fn from_graph_union(left: &Graph, right: &Graph) -> Self {
278 let terms = TermDictionary::new();
279 let mut triples = intern_graph(left, &terms);
280 triples.extend(intern_graph(right, &terms));
281 let default_graph = TripleIndex::build(triples);
282 let stats = DatasetStatistics::compute(&default_graph);
283 Self {
284 terms,
285 default_graph,
286 named_graphs: HashMap::new(),
287 reach_cache: RefCell::new(ReachCache::default()),
288 stats,
289 }
290 }
291
292 pub(crate) fn intern(&self, term: &Term) -> TermId {
296 self.terms.intern(term.clone())
297 }
298
299 pub(crate) fn plan_stats(&self) -> shifty_opt::PlanStats {
303 let predicate_cardinality: HashMap<Term, u64> = self
304 .stats
305 .predicate_cardinality
306 .iter()
307 .filter_map(|(&id, &count)| Some((self.externalize(id)?, count)))
308 .collect();
309 let distinct_predicates = predicate_cardinality.len() as u64;
310 shifty_opt::PlanStats {
311 total_triples: self.stats.triple_count,
312 distinct_subjects: self.stats.distinct_subjects,
313 distinct_objects: self.stats.distinct_objects,
314 distinct_predicates,
315 predicate_cardinality,
316 }
317 }
318
319 pub(crate) fn externalize(&self, id: TermId) -> Option<Term> {
322 self.terms.externalize(id)
323 }
324
325 pub(crate) fn externalize_id(&self, id: TermId) -> Term {
329 self.terms
330 .externalize(id)
331 .expect("scanned TermId originates from this dataset")
332 }
333
334 pub(crate) fn contains_ids(&self, subject: TermId, predicate: TermId, object: TermId) -> bool {
335 self.default_graph.contains(subject, predicate, object)
336 }
337
338 pub(crate) fn encode_triple(&self, triple: &oxrdf::Triple) -> [TermId; 3] {
339 [
340 self.terms.intern(triple.subject.clone().into()),
341 self.terms.intern(Term::NamedNode(triple.predicate.clone())),
342 self.terms.intern(triple.object.clone()),
343 ]
344 }
345
346 pub(crate) fn cached_reach(
347 &self,
348 node: TermId,
349 step: &ReachStep,
350 kind: ClosureKind,
351 graph: GraphSel,
352 ) -> Option<Rc<HashSet<TermId>>> {
353 self.reach_cache
354 .borrow()
355 .entries
356 .get(&ReachCacheKey {
357 node,
358 step: step.clone(),
359 kind,
360 graph,
361 })
362 .cloned()
363 }
364
365 pub(crate) fn cache_reach(
366 &self,
367 node: TermId,
368 step: &ReachStep,
369 kind: ClosureKind,
370 graph: GraphSel,
371 result: Rc<HashSet<TermId>>,
372 ) {
373 let mut cache = self.reach_cache.borrow_mut();
374 if cache.cached_ids.saturating_add(result.len()) > MAX_CACHED_REACH_IDS {
378 return;
379 }
380 let key = ReachCacheKey {
381 node,
382 step: step.clone(),
383 kind,
384 graph,
385 };
386 if cache.entries.contains_key(&key) {
387 return;
388 }
389 cache.cached_ids += result.len();
390 cache.entries.insert(key, result);
391 }
392
393 pub(crate) fn scan(
397 &self,
398 s: Option<TermId>,
399 p: Option<TermId>,
400 o: Option<TermId>,
401 graph: GraphSel,
402 ) -> Box<dyn Iterator<Item = [TermId; 3]> + '_> {
403 let iter = match graph {
404 GraphSel::Default => default_graph_quads(self, s, p, o),
405 GraphSel::Named(g) => named_graph_quads(self, g, s, p, o),
406 };
407 Box::new(iter.map(|q| {
408 let q = q.expect("infallible");
409 [q.subject, q.predicate, q.object]
410 }))
411 }
412
413 pub(crate) fn triples_for_predicate(
414 &self,
415 predicate: &NamedNode,
416 ) -> Box<dyn Iterator<Item = (Term, Term)> + '_> {
417 let p = self.intern(&Term::NamedNode(predicate.clone()));
418 Box::new(
419 self.scan(None, Some(p), None, GraphSel::Default)
420 .map(|[subject, _, object]| {
421 (self.externalize_id(subject), self.externalize_id(object))
422 }),
423 )
424 }
425
426 pub(crate) fn outgoing(
427 &self,
428 subject: &Term,
429 ) -> Box<dyn Iterator<Item = (NamedNode, Term)> + '_> {
430 let s = self.intern(subject);
431 Box::new(
432 self.scan(Some(s), None, None, GraphSel::Default)
433 .filter_map(|[_, predicate, object]| {
434 let Term::NamedNode(predicate) = self.externalize_id(predicate) else {
435 return None;
436 };
437 Some((predicate, self.externalize_id(object)))
438 }),
439 )
440 }
441
442 pub(crate) fn extend_triples<'a>(
444 &mut self,
445 triples: impl IntoIterator<Item = &'a oxrdf::Triple>,
446 ) {
447 let mut encoded: Vec<_> = triples
448 .into_iter()
449 .map(|triple| self.encode_triple(triple))
450 .collect();
451 encoded.sort_unstable();
452 encoded.dedup();
453 encoded.retain(|&[s, p, o]| !self.default_graph.contains(s, p, o));
454 self.stats.extend(&encoded);
455 self.default_graph.extend(encoded);
456 *self.reach_cache.borrow_mut() = ReachCache::default();
457 }
458
459 pub fn from_graphs(context: &Graph, shapes: &Graph) -> Self {
463 let terms = TermDictionary::new();
464 let triples = intern_graph(context, &terms);
465 let default_graph = TripleIndex::build(triples);
466 let stats = DatasetStatistics::compute(&default_graph);
467
468 let shapes_iri = NamedNode::new(SHAPES_GRAPH_IRI).expect("static IRI is valid");
469 let graph_id = terms.intern(Term::NamedNode(shapes_iri));
470 let mut named_triples = intern_graph(shapes, &terms);
471 named_triples.sort_unstable();
472 named_triples.dedup();
473
474 let mut named_graphs = HashMap::new();
475 named_graphs.insert(graph_id, named_triples);
476
477 Self {
478 terms,
479 default_graph,
480 named_graphs,
481 reach_cache: RefCell::new(ReachCache::default()),
482 stats,
483 }
484 }
485
486 pub fn from_graph_union_with_shapes(data: &Graph, shapes: &Graph) -> Self {
489 let terms = TermDictionary::new();
490 let mut triples = intern_graph(data, &terms);
491 triples.extend(intern_graph(shapes, &terms));
492 let default_graph = TripleIndex::build(triples);
493 let stats = DatasetStatistics::compute(&default_graph);
494
495 let shapes_iri = NamedNode::new(SHAPES_GRAPH_IRI).expect("static IRI is valid");
496 let graph_id = terms.intern(Term::NamedNode(shapes_iri));
497 let mut named_triples = intern_graph(shapes, &terms);
498 named_triples.sort_unstable();
499 named_triples.dedup();
500 let mut named_graphs = HashMap::new();
501 named_graphs.insert(graph_id, named_triples);
502
503 Self {
504 terms,
505 default_graph,
506 named_graphs,
507 reach_cache: RefCell::new(ReachCache::default()),
508 stats,
509 }
510 }
511}
512
513fn intern_graph(graph: &Graph, terms: &TermDictionary) -> Vec<[TermId; 3]> {
514 graph
515 .iter()
516 .map(|triple| {
517 let s = terms.intern(triple.subject.into_owned().into());
518 let p = terms.intern(Term::NamedNode(triple.predicate.into_owned()));
519 let o = terms.intern(triple.object.into_owned());
520 [s, p, o]
521 })
522 .collect()
523}
524
525#[allow(refining_impl_trait)]
528impl<'a> QueryableDataset<'a> for &'a FrozenIndexedDataset {
529 type InternalTerm = TermId;
530 type Error = Infallible;
531
532 fn internal_quads_for_pattern(
533 &self,
534 subject: Option<&TermId>,
535 predicate: Option<&TermId>,
536 object: Option<&TermId>,
537 graph_name: Option<Option<&TermId>>,
538 ) -> QuadIter<'a> {
539 #[allow(clippy::explicit_auto_deref)]
543 let ds: &'a FrozenIndexedDataset = *self;
544 let s = subject.copied();
545 let p = predicate.copied();
546 let o = object.copied();
547 match graph_name {
548 Some(None) => default_graph_quads(ds, s, p, o),
549 Some(Some(&g)) => named_graph_quads(ds, g, s, p, o),
550 None => all_named_quads(ds, s, p, o),
551 }
552 }
553
554 fn internalize_term(&self, term: Term) -> Result<TermId, Infallible> {
555 Ok(self.terms.intern(term))
559 }
560
561 fn externalize_term(&self, id: TermId) -> Result<Term, Infallible> {
562 Ok(self
563 .terms
564 .externalize(id)
565 .expect("TermId always originates from this dataset's internalize_term or internal_quads_for_pattern"))
566 }
567}
568
569type QuadIter<'a> = Box<dyn Iterator<Item = Result<InternalQuad<TermId>, Infallible>> + 'a>;
572
573fn mk_quad(s: TermId, p: TermId, o: TermId, g: Option<TermId>) -> InternalQuad<TermId> {
574 InternalQuad {
575 subject: s,
576 predicate: p,
577 object: o,
578 graph_name: g,
579 }
580}
581
582fn default_graph_quads<'a>(
584 ds: &'a FrozenIndexedDataset,
585 s: Option<TermId>,
586 p: Option<TermId>,
587 o: Option<TermId>,
588) -> QuadIter<'a> {
589 match (s, p, o) {
590 (Some(s), Some(p), Some(o)) => {
592 let hit = ds.default_graph.contains(s, p, o);
593 Box::new(hit.then(|| Ok(mk_quad(s, p, o, None))).into_iter())
594 }
595 (Some(s), Some(p), None) => {
597 let slice = ds.default_graph.range_sp(s, p);
598 Box::new(slice.iter().map(|&t| Ok(mk_quad(t[0], t[1], t[2], None))))
599 }
600 (Some(s), None, Some(o)) => {
602 let slice = ds.default_graph.range_s(s);
603 Box::new(
604 slice
605 .iter()
606 .filter(move |t| t[2] == o)
607 .map(|&t| Ok(mk_quad(t[0], t[1], t[2], None))),
608 )
609 }
610 (Some(s), None, None) => {
612 let slice = ds.default_graph.range_s(s);
613 Box::new(slice.iter().map(|&t| Ok(mk_quad(t[0], t[1], t[2], None))))
614 }
615 (None, Some(p), Some(o)) => {
617 let slice = ds.default_graph.range_po(p, o);
618 Box::new(slice.iter().map(|&t| Ok(mk_quad(t[2], t[0], t[1], None))))
619 }
620 (None, Some(p), None) => {
622 let slice = ds.default_graph.range_p(p);
623 Box::new(slice.iter().map(|&t| Ok(mk_quad(t[2], t[0], t[1], None))))
624 }
625 (None, None, Some(o)) => {
627 let slice = ds.default_graph.range_o(o);
628 Box::new(slice.iter().map(|&t| Ok(mk_quad(t[1], t[2], t[0], None))))
629 }
630 (None, None, None) => Box::new(
632 ds.default_graph
633 .spo
634 .iter()
635 .map(|&t| Ok(mk_quad(t[0], t[1], t[2], None))),
636 ),
637 }
638}
639
640fn named_graph_quads<'a>(
644 ds: &'a FrozenIndexedDataset,
645 g: TermId,
646 s: Option<TermId>,
647 p: Option<TermId>,
648 o: Option<TermId>,
649) -> QuadIter<'a> {
650 let Some(triples) = ds.named_graphs.get(&g) else {
651 return Box::new(std::iter::empty());
652 };
653 Box::new(
654 triples
655 .iter()
656 .filter(move |&&[ts, tp, to]| {
657 s.is_none_or(|v| ts == v) && p.is_none_or(|v| tp == v) && o.is_none_or(|v| to == v)
658 })
659 .map(move |&[ts, tp, to]| Ok(mk_quad(ts, tp, to, Some(g)))),
660 )
661}
662
663fn all_named_quads<'a>(
666 ds: &'a FrozenIndexedDataset,
667 s: Option<TermId>,
668 p: Option<TermId>,
669 o: Option<TermId>,
670) -> QuadIter<'a> {
671 Box::new(
672 ds.named_graphs
673 .keys()
674 .copied()
675 .flat_map(move |g| named_graph_quads(ds, g, s, p, o)),
676 )
677}
678
679#[cfg(test)]
682mod tests {
683 use super::*;
684 use oxrdf::{NamedNode, Triple};
685 use spareval::QueryableDataset;
686
687 fn nn(iri: &str) -> NamedNode {
688 NamedNode::new(iri).unwrap()
689 }
690
691 fn triple_nnn(s: &str, p: &str, o: &str) -> Triple {
692 Triple::new(nn(s), nn(p), nn(o))
693 }
694
695 fn small_graph() -> Graph {
696 let mut g = Graph::new();
697 g.insert(triple_nnn("http://ex/a", "http://ex/p", "http://ex/b").as_ref());
698 g.insert(triple_nnn("http://ex/a", "http://ex/p", "http://ex/c").as_ref());
699 g.insert(triple_nnn("http://ex/b", "http://ex/q", "http://ex/c").as_ref());
700 g
701 }
702
703 #[test]
704 fn intern_round_trips() {
705 let g = small_graph();
706 let ds = FrozenIndexedDataset::from_graph(&g);
707 let a = Term::NamedNode(nn("http://ex/a"));
708 let id = ds.terms.intern(a.clone());
709 assert_eq!(ds.terms.externalize(id), Some(a));
710 }
711
712 #[test]
713 fn contains_triple() {
714 let g = small_graph();
715 let ds = FrozenIndexedDataset::from_graph(&g);
716 let s = ds.terms.get(&Term::NamedNode(nn("http://ex/a"))).unwrap();
717 let p = ds.terms.get(&Term::NamedNode(nn("http://ex/p"))).unwrap();
718 let o = ds.terms.get(&Term::NamedNode(nn("http://ex/b"))).unwrap();
719 assert!(ds.default_graph.contains(s, p, o));
720 }
721
722 #[test]
723 fn missing_triple_not_found() {
724 let g = small_graph();
725 let ds = FrozenIndexedDataset::from_graph(&g);
726 let s = ds.terms.get(&Term::NamedNode(nn("http://ex/a"))).unwrap();
727 let p = ds.terms.get(&Term::NamedNode(nn("http://ex/q"))).unwrap();
728 let o = ds.terms.get(&Term::NamedNode(nn("http://ex/c"))).unwrap();
729 assert!(!ds.default_graph.contains(s, p, o));
730 }
731
732 #[test]
733 fn range_s_returns_correct_triples() {
734 let g = small_graph();
735 let ds = FrozenIndexedDataset::from_graph(&g);
736 let s = ds.terms.get(&Term::NamedNode(nn("http://ex/a"))).unwrap();
737 let slice = ds.default_graph.range_s(s);
738 assert_eq!(slice.len(), 2); }
740
741 #[test]
742 fn extend_triples_updates_indexes_and_statistics() {
743 let g = small_graph();
744 let mut ds = FrozenIndexedDataset::from_graph(&g);
745 let added = triple_nnn("http://ex/new", "http://ex/p", "http://ex/b");
746 ds.extend_triples([&added, &added]);
747
748 let s = ds.intern(&Term::NamedNode(nn("http://ex/new")));
749 let p = ds.intern(&Term::NamedNode(nn("http://ex/p")));
750 let o = ds.intern(&Term::NamedNode(nn("http://ex/b")));
751 assert!(ds.contains_ids(s, p, o));
752 assert_eq!(ds.stats.triple_count, 4);
753 assert_eq!(ds.stats.predicate_cardinality[&p], 3);
754 }
755
756 #[test]
757 fn internalize_unknown_term_gets_unique_id() {
758 let g = small_graph();
759 let ds = FrozenIndexedDataset::from_graph(&g);
760 let rds: &FrozenIndexedDataset = &ds;
761 let unknown1 = Term::NamedNode(nn("http://ex/unknown1"));
762 let unknown2 = Term::NamedNode(nn("http://ex/unknown2"));
763 let id1 = rds.internalize_term(unknown1.clone()).unwrap();
764 let id2 = rds.internalize_term(unknown2.clone()).unwrap();
765 assert_ne!(id1, id2, "different unknown terms must get different IDs");
766 let id1b = rds.internalize_term(unknown1).unwrap();
768 assert_eq!(id1, id1b);
769 }
770
771 #[test]
772 fn queryable_dataset_default_graph_pattern() {
773 let g = small_graph();
774 let ds = FrozenIndexedDataset::from_graph(&g);
775 let rds: &FrozenIndexedDataset = &ds;
776 let quads: Vec<_> = rds
778 .internal_quads_for_pattern(None, None, None, Some(None))
779 .map(|r| r.unwrap())
780 .collect();
781 assert_eq!(quads.len(), 3);
782 }
783
784 #[test]
785 fn queryable_dataset_named_graph_empty_when_not_loaded() {
786 let g = small_graph();
787 let ds = FrozenIndexedDataset::from_graph(&g);
788 let rds: &FrozenIndexedDataset = &ds;
789 let quads: Vec<_> = rds
791 .internal_quads_for_pattern(None, None, None, None)
792 .collect();
793 assert!(quads.is_empty());
794 }
795
796 #[test]
797 fn from_graphs_loads_both_default_and_named() {
798 let data = small_graph();
799 let mut shapes = Graph::new();
800 shapes.insert(
801 triple_nnn(
802 "http://ex/S",
803 "http://www.w3.org/ns/shacl#targetNode",
804 "http://ex/a",
805 )
806 .as_ref(),
807 );
808 let ds = FrozenIndexedDataset::from_graphs(&data, &shapes);
809 let rds: &FrozenIndexedDataset = &ds;
810
811 let default_quads: Vec<_> = rds
813 .internal_quads_for_pattern(None, None, None, Some(None))
814 .map(|r| r.unwrap())
815 .collect();
816 assert_eq!(default_quads.len(), 3);
817
818 let shapes_iri_id = ds
820 .terms
821 .get(&Term::NamedNode(nn(SHAPES_GRAPH_IRI)))
822 .expect("shapes graph IRI should be interned");
823 let named_quads: Vec<_> = rds
824 .internal_quads_for_pattern(None, None, None, Some(Some(&shapes_iri_id)))
825 .map(|r| r.unwrap())
826 .collect();
827 assert_eq!(named_quads.len(), 1);
828 }
829
830 #[test]
831 fn graph_union_builds_one_deduplicated_default_graph() {
832 let left = small_graph();
833 let mut right = Graph::new();
834 right.insert(triple_nnn("http://ex/a", "http://ex/p", "http://ex/b").as_ref());
835 right.insert(triple_nnn("http://ex/new", "http://ex/p", "http://ex/b").as_ref());
836
837 let ds = FrozenIndexedDataset::from_graph_union(&left, &right);
838
839 assert_eq!(ds.stats.triple_count, 4);
840 }
841}