1use std::cell::RefCell;
19use std::collections::{BTreeMap, HashMap, HashSet};
20use std::convert::Infallible;
21use std::rc::Rc;
22use std::sync::Arc;
23use std::sync::OnceLock;
24use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
25
26use crate::path_plan::ReachStep;
27use crate::profile::IndexRecord;
28use oxrdf::{BlankNode, Graph, NamedNode, Term};
29use shifty_opt::{AccessCatalog, ClosureKind};
30use spareval::{InternalQuad, QueryableDataset};
31use web_time::Instant;
32
33pub(crate) const SHAPES_GRAPH_IRI: &str = "urn:x-shacl:shapes-graph";
35
36pub type TermId = u32;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub(crate) enum GraphSel {
43 Default,
45 Named(TermId),
47}
48
49struct TermDictInner {
52 id_to_term: Vec<Term>,
53 term_to_id: HashMap<Term, TermId>,
54}
55
56struct TermDictionary {
60 source: Option<Arc<SourceStorage>>,
61 local: RefCell<TermDictInner>,
62 local_blank_aliases: RefCell<HashMap<BlankNode, BlankNode>>,
65}
66
67pub(crate) struct SourceStorage {
70 id_to_term: Vec<Term>,
71 term_to_id: HashMap<Term, TermId>,
72 index: Arc<TripleIndex>,
73 reverse_demand: Vec<NamedNode>,
74 subject_demand: bool,
75 object_demand: bool,
76}
77
78impl SourceStorage {
79 #[cfg(test)]
80 pub(crate) fn encode(graph: &Graph) -> Arc<Self> {
81 Self::encode_with_demand(graph, &AccessCatalog::default())
82 }
83
84 pub(crate) fn encode_with_demand(graph: &Graph, catalog: &AccessCatalog) -> Arc<Self> {
85 let started = crate::profile::is_enabled().then(Instant::now);
86 let terms = TermDictionary::new();
87 let triples = intern_graph(graph, &terms);
88 let reverse_demand: HashSet<_> = catalog
89 .consumers
90 .iter()
91 .filter(|consumer| consumer.default.probes.reverse || consumer.shapes.probes.reverse)
92 .flat_map(|consumer| {
93 consumer
94 .default
95 .predicates
96 .iter()
97 .chain(&consumer.shapes.predicates)
98 })
99 .cloned()
100 .collect();
101 let source_demand = reverse_demand
102 .iter()
103 .filter_map(|p| terms.get(&Term::NamedNode(p.clone())))
104 .collect();
105 let predicate_labels = predicate_labels(&triples, &terms);
106 let subject_demand = catalog.consumers.iter().any(|consumer| {
107 (consumer.default.any_predicate && consumer.default.probes.forward)
108 || (consumer.shapes.any_predicate && consumer.shapes.probes.forward)
109 });
110 let object_demand = catalog.consumers.iter().any(|consumer| {
111 (consumer.default.any_predicate && consumer.default.probes.reverse)
112 || (consumer.shapes.any_predicate && consumer.shapes.probes.reverse)
113 });
114 let index = Arc::new(TripleIndex::build_with_scope(
115 triples,
116 IndexPolicy::DemandDriven,
117 SOURCE_INDEX_BUDGET,
118 source_demand,
119 subject_demand,
120 object_demand,
121 IndexScope::Source,
122 predicate_labels,
123 ));
124 let local = terms.local.into_inner();
125 crate::profile::record_source_build(index.len());
126 if let Some(started) = started {
127 crate::profile::record_source_encode_time(started.elapsed().as_micros() as u64);
128 }
129 Arc::new(Self {
130 id_to_term: local.id_to_term,
131 term_to_id: local.term_to_id,
132 index,
133 reverse_demand: reverse_demand.into_iter().collect(),
134 subject_demand,
135 object_demand,
136 })
137 }
138}
139
140impl TermDictionary {
141 fn new() -> Self {
142 Self {
143 source: None,
144 local: RefCell::new(TermDictInner {
145 id_to_term: Vec::new(),
146 term_to_id: HashMap::new(),
147 }),
148 local_blank_aliases: RefCell::new(HashMap::new()),
149 }
150 }
151
152 fn from_source(source: Arc<SourceStorage>) -> Self {
153 Self {
154 source: Some(source),
155 local: RefCell::new(TermDictInner {
156 id_to_term: Vec::new(),
157 term_to_id: HashMap::new(),
158 }),
159 local_blank_aliases: RefCell::new(HashMap::new()),
160 }
161 }
162
163 fn intern(&self, term: Term) -> TermId {
165 if let Some(id) = self.source.as_ref().and_then(|s| s.term_to_id.get(&term)) {
166 return *id;
167 }
168 let mut inner = self.local.borrow_mut();
169 if let Some(&id) = inner.term_to_id.get(&term) {
170 return id;
171 }
172 let source_len = self.source.as_ref().map_or(0, |s| s.id_to_term.len());
173 let id = TermId::try_from(source_len + inner.id_to_term.len())
174 .expect("RDF term dictionary exceeded u32 ID capacity");
175 inner.term_to_id.insert(term.clone(), id);
176 inner.id_to_term.push(term);
177 id
178 }
179
180 fn intern_data(&self, term: Term) -> TermId {
182 let Term::BlankNode(blank) = term else {
183 return self.intern(term);
184 };
185 let source_term = Term::BlankNode(blank.clone());
186 if !self
187 .source
188 .as_ref()
189 .is_some_and(|source| source.term_to_id.contains_key(&source_term))
190 {
191 return self.intern(source_term);
192 }
193 if let Some(alias) = self.local_blank_aliases.borrow().get(&blank).cloned() {
194 return self.intern(Term::BlankNode(alias));
195 }
196 let alias = loop {
197 let candidate = BlankNode::default();
198 if self.get(&Term::BlankNode(candidate.clone())).is_none() {
199 break candidate;
200 }
201 };
202 self.local_blank_aliases
203 .borrow_mut()
204 .insert(blank, alias.clone());
205 self.intern(Term::BlankNode(alias))
206 }
207
208 fn get(&self, term: &Term) -> Option<TermId> {
209 self.source
210 .as_ref()
211 .and_then(|s| s.term_to_id.get(term).copied())
212 .or_else(|| self.local.borrow().term_to_id.get(term).copied())
213 }
214
215 fn externalize(&self, id: TermId) -> Option<Term> {
216 let source_len = self.source.as_ref().map_or(0, |s| s.id_to_term.len());
217 if (id as usize) < source_len {
218 return self.source.as_ref()?.id_to_term.get(id as usize).cloned();
219 }
220 self.local
221 .borrow()
222 .id_to_term
223 .get(id as usize - source_len)
224 .cloned()
225 }
226
227 fn blank_node(&self, id: TermId) -> Option<BlankNode> {
228 let source_len = self.source.as_ref().map_or(0, |s| s.id_to_term.len());
229 if (id as usize) < source_len {
230 return match self.source.as_ref()?.id_to_term.get(id as usize) {
231 Some(Term::BlankNode(node)) => Some(node.clone()),
232 _ => None,
233 };
234 }
235 match self.local.borrow().id_to_term.get(id as usize - source_len) {
236 Some(Term::BlankNode(node)) => Some(node.clone()),
237 _ => None,
238 }
239 }
240}
241
242struct TripleIndex {
249 partitions: BTreeMap<TermId, PredicatePartition>,
250 len: usize,
251 policy: IndexPolicy,
252 reverse_demand: HashSet<TermId>,
253 secondary_budget: usize,
254 secondary_bytes: AtomicUsize,
255 subject_dir: OnceLock<Option<Vec<[TermId; 3]>>>,
256 object_dir: OnceLock<Option<Vec<[TermId; 3]>>>,
257 subject_probes: AtomicU32,
258 object_probes: AtomicU32,
259 subject_demand: bool,
260 object_demand: bool,
261 scope: IndexScope,
262 predicate_labels: HashMap<TermId, String>,
263}
264
265struct PredicatePartition {
266 pairs: Vec<[TermId; 2]>,
267 reverse: OnceLock<Option<Vec<[TermId; 2]>>>,
268 reverse_probes: AtomicU32,
269}
270
271impl Default for PredicatePartition {
272 fn default() -> Self {
273 Self {
274 pairs: Vec::new(),
275 reverse: OnceLock::new(),
276 reverse_probes: AtomicU32::new(0),
277 }
278 }
279}
280
281#[derive(Clone, Copy, Debug, PartialEq, Eq)]
282enum IndexPolicy {
283 BaseOnly,
284 AllIndexes,
285 DemandDriven,
286}
287
288#[derive(Clone, Copy)]
289enum IndexScope {
290 Source,
291 Session,
292}
293
294impl IndexScope {
295 fn label(self) -> &'static str {
296 match self {
297 Self::Source => "source",
298 Self::Session => "session",
299 }
300 }
301}
302
303const SOURCE_INDEX_BUDGET: usize = 64 * 1024 * 1024;
304const SESSION_INDEX_BUDGET: usize = 32 * 1024 * 1024;
305
306fn merge_sorted<T: Copy + Ord>(existing: &mut Vec<T>, mut incoming: Vec<T>) {
307 incoming.sort_unstable();
308 incoming.dedup();
309 let mut merged = Vec::with_capacity(existing.len() + incoming.len());
310 let (mut a, mut b) = (0, 0);
311 while a < existing.len() && b < incoming.len() {
312 match existing[a].cmp(&incoming[b]) {
313 std::cmp::Ordering::Less => {
314 merged.push(existing[a]);
315 a += 1;
316 }
317 std::cmp::Ordering::Greater => {
318 merged.push(incoming[b]);
319 b += 1;
320 }
321 std::cmp::Ordering::Equal => {
322 merged.push(existing[a]);
323 a += 1;
324 b += 1;
325 }
326 }
327 }
328 merged.extend_from_slice(&existing[a..]);
329 merged.extend_from_slice(&incoming[b..]);
330 *existing = merged;
331}
332
333impl TripleIndex {
334 fn build(triples: Vec<[TermId; 3]>) -> Self {
335 Self::build_with_policy(
336 triples,
337 IndexPolicy::DemandDriven,
338 SESSION_INDEX_BUDGET,
339 HashSet::new(),
340 false,
341 false,
342 )
343 }
344
345 fn build_with_policy(
346 triples: Vec<[TermId; 3]>,
347 policy: IndexPolicy,
348 secondary_budget: usize,
349 reverse_demand: HashSet<TermId>,
350 subject_demand: bool,
351 object_demand: bool,
352 ) -> Self {
353 Self::build_with_scope(
354 triples,
355 policy,
356 secondary_budget,
357 reverse_demand,
358 subject_demand,
359 object_demand,
360 IndexScope::Session,
361 HashMap::new(),
362 )
363 }
364
365 #[allow(clippy::too_many_arguments)]
366 fn build_with_scope(
367 triples: Vec<[TermId; 3]>,
368 policy: IndexPolicy,
369 secondary_budget: usize,
370 reverse_demand: HashSet<TermId>,
371 subject_demand: bool,
372 object_demand: bool,
373 scope: IndexScope,
374 predicate_labels: HashMap<TermId, String>,
375 ) -> Self {
376 let mut partitions: BTreeMap<TermId, PredicatePartition> = BTreeMap::new();
377 for [s, p, o] in triples {
378 partitions.entry(p).or_default().pairs.push([s, o]);
379 }
380 let mut len = 0;
381 for partition in partitions.values_mut() {
382 partition.pairs.sort_unstable();
383 partition.pairs.dedup();
384 len += partition.pairs.len();
385 }
386 let index = Self {
387 partitions,
388 len,
389 policy,
390 reverse_demand,
391 secondary_budget,
392 secondary_bytes: AtomicUsize::new(0),
393 subject_dir: OnceLock::new(),
394 object_dir: OnceLock::new(),
395 subject_probes: AtomicU32::new(0),
396 object_probes: AtomicU32::new(0),
397 subject_demand,
398 object_demand,
399 scope,
400 predicate_labels,
401 };
402 if crate::profile::is_enabled() {
403 crate::profile::record_primary_index_bytes(
404 index.scope.label(),
405 0,
406 index.primary_bytes(),
407 );
408 }
409 if policy == IndexPolicy::AllIndexes {
410 for (&predicate, partition) in &index.partitions {
411 index.reverse(predicate, partition);
412 }
413 index.subject_directory();
414 index.object_directory();
415 }
416 index
417 }
418
419 fn contains(&self, s: TermId, p: TermId, o: TermId) -> bool {
420 self.partitions
421 .get(&p)
422 .is_some_and(|partition| partition.pairs.binary_search(&[s, o]).is_ok())
423 }
424
425 fn extend(&mut self, triples: Vec<[TermId; 3]>) {
426 let old_primary_bytes = crate::profile::is_enabled().then(|| self.primary_bytes());
427 let mut batches: BTreeMap<TermId, Vec<[TermId; 2]>> = BTreeMap::new();
428 let mut added_rows = Vec::new();
429 for [s, p, o] in triples {
430 batches.entry(p).or_default().push([s, o]);
431 }
432 for (predicate, mut incoming) in batches {
433 incoming.sort_unstable();
434 incoming.dedup();
435 let partition = self.partitions.entry(predicate).or_default();
436 let existing = &mut partition.pairs;
437 if existing.is_empty() {
438 self.len += incoming.len();
439 added_rows.extend(incoming.iter().map(|&[s, o]| [s, predicate, o]));
440 *existing = incoming;
441 continue;
442 }
443 let additions: Vec<_> = incoming
444 .iter()
445 .copied()
446 .filter(|row| existing.binary_search(row).is_err())
447 .collect();
448 self.len += additions.len();
449 added_rows.extend(additions.iter().map(|&[s, o]| [s, predicate, o]));
450 merge_sorted(existing, incoming);
451 if let Some(Some(reverse)) = partition.reverse.get_mut() {
452 let bytes = additions.len() * std::mem::size_of::<[TermId; 2]>();
453 if self
454 .secondary_bytes
455 .load(Ordering::Relaxed)
456 .saturating_add(bytes)
457 <= self.secondary_budget
458 {
459 self.secondary_bytes.fetch_add(bytes, Ordering::Relaxed);
460 merge_sorted(
461 reverse,
462 additions.into_iter().map(|[s, o]| [o, s]).collect(),
463 );
464 } else {
465 let old_bytes = reverse.len() * std::mem::size_of::<[TermId; 2]>();
466 partition.reverse.take();
467 self.secondary_bytes.fetch_sub(old_bytes, Ordering::Relaxed);
468 }
469 }
470 }
471 if let Some(Some(directory)) = self.subject_dir.get_mut() {
472 let bytes = added_rows.len() * std::mem::size_of::<[TermId; 3]>();
473 if self
474 .secondary_bytes
475 .load(Ordering::Relaxed)
476 .saturating_add(bytes)
477 <= self.secondary_budget
478 {
479 self.secondary_bytes.fetch_add(bytes, Ordering::Relaxed);
480 merge_sorted(directory, added_rows.clone());
481 } else {
482 let old_bytes = directory.len() * std::mem::size_of::<[TermId; 3]>();
483 self.subject_dir.take();
484 self.secondary_bytes.fetch_sub(old_bytes, Ordering::Relaxed);
485 }
486 }
487 if let Some(Some(directory)) = self.object_dir.get_mut() {
488 let bytes = added_rows.len() * std::mem::size_of::<[TermId; 3]>();
489 if self
490 .secondary_bytes
491 .load(Ordering::Relaxed)
492 .saturating_add(bytes)
493 <= self.secondary_budget
494 {
495 self.secondary_bytes.fetch_add(bytes, Ordering::Relaxed);
496 merge_sorted(
497 directory,
498 added_rows.into_iter().map(|[s, p, o]| [o, p, s]).collect(),
499 );
500 } else {
501 let old_bytes = directory.len() * std::mem::size_of::<[TermId; 3]>();
502 self.object_dir.take();
503 self.secondary_bytes.fetch_sub(old_bytes, Ordering::Relaxed);
504 }
505 }
506 if self.policy == IndexPolicy::AllIndexes {
507 for (&predicate, partition) in &self.partitions {
508 self.reverse(predicate, partition);
509 }
510 }
511 if let Some(old_primary_bytes) = old_primary_bytes {
512 crate::profile::record_primary_index_bytes(
513 self.scope.label(),
514 old_primary_bytes,
515 self.primary_bytes(),
516 );
517 }
518 }
519
520 fn primary_bytes(&self) -> usize {
521 self.partitions
522 .values()
523 .map(|partition| partition.pairs.capacity() * std::mem::size_of::<[TermId; 2]>())
524 .sum()
525 }
526
527 fn len(&self) -> usize {
528 self.len
529 }
530
531 fn rows(&self) -> impl Iterator<Item = [TermId; 3]> + '_ {
532 self.partitions
533 .iter()
534 .flat_map(|(&p, partition)| partition.pairs.iter().map(move |&[s, o]| [s, p, o]))
535 }
536
537 fn reserve_secondary(&self, bytes: usize) -> bool {
538 self.secondary_bytes
539 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |used| {
540 used.checked_add(bytes)
541 .filter(|total| *total <= self.secondary_budget)
542 })
543 .is_ok()
544 }
545
546 fn record_decision(
547 &self,
548 kind: &'static str,
549 predicate: Option<TermId>,
550 reason: &'static str,
551 rows: usize,
552 bytes: usize,
553 built: Option<(u64, usize)>,
554 ) {
555 crate::profile::record_index_decision(IndexRecord {
556 scope: self.scope.label(),
557 kind,
558 predicate: predicate.and_then(|id| self.predicate_labels.get(&id).cloned()),
559 reason,
560 rows,
561 estimated_bytes: bytes,
562 actual_bytes: built.map_or(0, |(_, bytes)| bytes),
563 budget_bytes: self.secondary_budget,
564 build_us: built.map_or(0, |(us, _)| us),
565 accepted: built.is_some(),
566 });
567 }
568
569 fn reverse<'a>(
570 &self,
571 predicate: TermId,
572 partition: &'a PredicatePartition,
573 ) -> Option<&'a Vec<[TermId; 2]>> {
574 if self.policy == IndexPolicy::BaseOnly {
575 return None;
576 }
577 let probes = partition.reverse_probes.fetch_add(1, Ordering::Relaxed) + 1;
578 if self.policy == IndexPolicy::DemandDriven
579 && (partition.pairs.len() < 64
580 || (probes < 3 && !self.reverse_demand.contains(&predicate)))
581 {
582 return None;
583 }
584 partition
585 .reverse
586 .get_or_init(|| {
587 let bytes = partition.pairs.len() * std::mem::size_of::<[TermId; 2]>();
588 let reason = if self.policy == IndexPolicy::AllIndexes {
589 "full-index policy"
590 } else if self.reverse_demand.contains(&predicate) {
591 "compiled demand"
592 } else {
593 "observed probes"
594 };
595 if !self.reserve_secondary(bytes) {
596 self.record_decision(
597 "reverse",
598 Some(predicate),
599 reason,
600 partition.pairs.len(),
601 bytes,
602 None,
603 );
604 return None;
605 }
606 let started = web_time::Instant::now();
607 let mut rows: Vec<_> = partition.pairs.iter().map(|&[s, o]| [o, s]).collect();
608 rows.sort_unstable();
609 self.record_decision(
610 "reverse",
611 Some(predicate),
612 reason,
613 rows.len(),
614 bytes,
615 Some((
616 started.elapsed().as_micros() as u64,
617 rows.capacity() * std::mem::size_of::<[TermId; 2]>(),
618 )),
619 );
620 Some(rows)
621 })
622 .as_ref()
623 }
624
625 fn subject_directory(&self) -> Option<&Vec<[TermId; 3]>> {
626 if self.policy == IndexPolicy::BaseOnly {
627 return None;
628 }
629 let probes = self.subject_probes.fetch_add(1, Ordering::Relaxed) + 1;
630 if self.policy == IndexPolicy::DemandDriven
631 && (self.len < 256 || (probes < 3 && !self.subject_demand))
632 {
633 return None;
634 }
635 self.subject_dir
636 .get_or_init(|| {
637 let bytes = self.len * std::mem::size_of::<[TermId; 3]>();
638 let reason = if self.policy == IndexPolicy::AllIndexes {
639 "full-index policy"
640 } else if self.subject_demand {
641 "compiled demand"
642 } else {
643 "observed probes"
644 };
645 if !self.reserve_secondary(bytes) {
646 self.record_decision("subject", None, reason, self.len, bytes, None);
647 return None;
648 }
649 let started = web_time::Instant::now();
650 let mut rows = Vec::with_capacity(self.len);
651 rows.extend(self.rows());
652 rows.sort_unstable();
653 self.record_decision(
654 "subject",
655 None,
656 reason,
657 rows.len(),
658 bytes,
659 Some((
660 started.elapsed().as_micros() as u64,
661 rows.capacity() * std::mem::size_of::<[TermId; 3]>(),
662 )),
663 );
664 Some(rows)
665 })
666 .as_ref()
667 }
668
669 fn object_directory(&self) -> Option<&Vec<[TermId; 3]>> {
670 if self.policy == IndexPolicy::BaseOnly {
671 return None;
672 }
673 let probes = self.object_probes.fetch_add(1, Ordering::Relaxed) + 1;
674 if self.policy == IndexPolicy::DemandDriven
675 && (self.len < 256 || (probes < 3 && !self.object_demand))
676 {
677 return None;
678 }
679 self.object_dir
680 .get_or_init(|| {
681 let bytes = self.len * std::mem::size_of::<[TermId; 3]>();
682 let reason = if self.policy == IndexPolicy::AllIndexes {
683 "full-index policy"
684 } else if self.object_demand {
685 "compiled demand"
686 } else {
687 "observed probes"
688 };
689 if !self.reserve_secondary(bytes) {
690 self.record_decision("object", None, reason, self.len, bytes, None);
691 return None;
692 }
693 let started = web_time::Instant::now();
694 let mut rows = Vec::with_capacity(self.len);
695 rows.extend(self.rows().map(|[s, p, o]| [o, p, s]));
696 rows.sort_unstable();
697 self.record_decision(
698 "object",
699 None,
700 reason,
701 rows.len(),
702 bytes,
703 Some((
704 started.elapsed().as_micros() as u64,
705 rows.capacity() * std::mem::size_of::<[TermId; 3]>(),
706 )),
707 );
708 Some(rows)
709 })
710 .as_ref()
711 }
712
713 fn scan(
714 &self,
715 s: Option<TermId>,
716 p: Option<TermId>,
717 o: Option<TermId>,
718 ) -> Box<dyn Iterator<Item = [TermId; 3]> + '_> {
719 let mask = usize::from(s.is_some())
720 | (usize::from(p.is_some()) << 1)
721 | (usize::from(o.is_some()) << 2);
722 let record = |candidates| crate::profile::record_scan(self.scope.label(), mask, candidates);
723 if let Some(p) = p {
724 let Some(partition) = self.partitions.get(&p) else {
725 record(0);
726 return Box::new(std::iter::empty());
727 };
728 if s.is_none()
729 && let Some(o) = o
730 && let Some(reverse) = self.reverse(p, partition)
731 {
732 let lo = reverse.partition_point(|row| row[0] < o);
733 let hi = reverse.partition_point(|row| row[0] <= o);
734 record(hi - lo);
735 return Box::new(reverse[lo..hi].iter().map(move |&[o, s]| [s, p, o]));
736 }
737 let pairs = &partition.pairs;
738 let pairs = if let Some(s) = s {
739 let lo = pairs.partition_point(|row| row[0] < s);
740 let hi = pairs.partition_point(|row| row[0] <= s);
741 &pairs[lo..hi]
742 } else {
743 pairs.as_slice()
744 };
745 record(pairs.len());
746 Box::new(
747 pairs
748 .iter()
749 .filter(move |row| o.is_none_or(|o| row[1] == o))
750 .map(move |&[s, o]| [s, p, o]),
751 )
752 } else {
753 if let Some(s) = s
754 && let Some(directory) = self.subject_directory()
755 {
756 let lo = directory.partition_point(|row| row[0] < s);
757 let hi = directory.partition_point(|row| row[0] <= s);
758 record(hi - lo);
759 return Box::new(
760 directory[lo..hi]
761 .iter()
762 .filter(move |row| o.is_none_or(|o| row[2] == o))
763 .copied(),
764 );
765 }
766 if let Some(o) = o
767 && let Some(directory) = self.object_directory()
768 {
769 let lo = directory.partition_point(|row| row[0] < o);
770 let hi = directory.partition_point(|row| row[0] <= o);
771 record(hi - lo);
772 return Box::new(
773 directory[lo..hi]
774 .iter()
775 .filter(move |row| s.is_none_or(|s| row[2] == s))
776 .map(|&[o, p, s]| [s, p, o]),
777 );
778 }
779 record(self.len);
780 Box::new(self.rows().filter(move |&[subject, _, object]| {
781 s.is_none_or(|s| subject == s) && o.is_none_or(|o| object == o)
782 }))
783 }
784 }
785}
786
787pub struct DatasetStatistics {
791 pub triple_count: u64,
792 pub distinct_subjects: u64,
793 pub distinct_objects: u64,
794 pub predicate_cardinality: HashMap<TermId, u64>,
796 subjects: HashSet<TermId>,
797 objects: HashSet<TermId>,
798}
799
800impl DatasetStatistics {
801 fn compute(triples: &TripleIndex) -> Self {
802 Self::compute_rows(triples.rows())
803 }
804
805 fn compute_rows(rows: impl Iterator<Item = [TermId; 3]>) -> Self {
806 let mut triple_count = 0;
807 let mut subjects = HashSet::new();
808 let mut objects = HashSet::new();
809 let mut predicate_cardinality: HashMap<TermId, u64> = HashMap::new();
810 for [s, p, o] in rows {
811 triple_count += 1;
812 subjects.insert(s);
813 objects.insert(o);
814 *predicate_cardinality.entry(p).or_insert(0) += 1;
815 }
816
817 Self {
818 triple_count,
819 distinct_subjects: subjects.len() as u64,
820 distinct_objects: objects.len() as u64,
821 predicate_cardinality,
822 subjects,
823 objects,
824 }
825 }
826
827 fn extend(&mut self, triples: &[[TermId; 3]]) {
828 self.triple_count += triples.len() as u64;
829 for &[subject, predicate, object] in triples {
830 self.subjects.insert(subject);
831 self.objects.insert(object);
832 *self.predicate_cardinality.entry(predicate).or_insert(0) += 1;
833 }
834 self.distinct_subjects = self.subjects.len() as u64;
835 self.distinct_objects = self.objects.len() as u64;
836 }
837}
838
839enum SourceMembership {
843 All,
844 Included(HashSet<[TermId; 3]>),
845 Excluded(HashSet<[TermId; 3]>),
846}
847
848struct SourceView {
849 index: Arc<TripleIndex>,
850 membership: SourceMembership,
851 include_all: bool,
852}
853
854impl SourceView {
855 fn data_member(&self, row: [TermId; 3]) -> bool {
856 match &self.membership {
857 SourceMembership::All => true,
858 SourceMembership::Included(rows) => rows.contains(&row),
859 SourceMembership::Excluded(rows) => !rows.contains(&row),
860 }
861 }
862
863 fn includes(&self, row: [TermId; 3]) -> bool {
864 self.include_all || self.data_member(row)
865 }
866}
867
868pub struct FrozenIndexedDataset {
874 terms: TermDictionary,
875 default_graph: TripleIndex,
877 source_view: Option<SourceView>,
878 named_graphs: HashMap<TermId, Arc<TripleIndex>>,
881 reach_cache: RefCell<ReachCache>,
882 revision: u64,
885 pub stats: DatasetStatistics,
886}
887
888const MAX_CACHED_REACH_IDS: usize = 1_000_000;
894
895#[derive(PartialEq, Eq, Hash)]
896struct ReachCacheKey {
897 node: TermId,
898 step: ReachStep,
899 kind: ClosureKind,
900 graph: GraphSel,
901}
902
903#[derive(Default)]
904struct ReachCache {
905 entries: HashMap<ReachCacheKey, Rc<HashSet<TermId>>>,
909 cached_ids: usize,
910}
911
912impl FrozenIndexedDataset {
913 pub(crate) fn from_data_with_source(
918 data: &Graph,
919 source: Arc<SourceStorage>,
920 separate: bool,
921 include_source: bool,
922 ) -> Self {
923 let started = crate::profile::is_enabled().then(Instant::now);
924 if started.is_some() {
925 crate::profile::observe_shared_source_primary_bytes(source.index.primary_bytes());
926 }
927 let terms = TermDictionary::from_source(Arc::clone(&source));
928 let mut local = Vec::new();
929 let mut overlapping = HashSet::new();
930 let data_rows = if separate {
931 intern_data_graph(data, &terms)
932 } else {
933 intern_graph(data, &terms)
934 };
935 for row in data_rows {
936 if source.index.contains(row[0], row[1], row[2]) {
937 overlapping.insert(row);
938 } else {
939 local.push(row);
940 }
941 }
942 let local_demand = source
943 .reverse_demand
944 .iter()
945 .map(|p| terms.intern(Term::NamedNode(p.clone())))
946 .collect();
947 let predicate_labels = predicate_labels(&local, &terms);
948 let default_graph = TripleIndex::build_with_scope(
949 local,
950 IndexPolicy::DemandDriven,
951 SESSION_INDEX_BUDGET,
952 local_demand,
953 source.subject_demand,
954 source.object_demand,
955 IndexScope::Session,
956 predicate_labels,
957 );
958 let membership = if overlapping.len() == source.index.len() {
959 SourceMembership::All
960 } else if overlapping.len() > source.index.len() / 2 {
961 let excluded = source
962 .index
963 .rows()
964 .filter(|row| !overlapping.contains(row))
965 .collect();
966 SourceMembership::Excluded(excluded)
967 } else {
968 SourceMembership::Included(overlapping)
969 };
970 let source_view = SourceView {
971 index: Arc::clone(&source.index),
972 membership,
973 include_all: include_source,
974 };
975 let stats = DatasetStatistics::compute_rows(
976 default_graph.rows().chain(
977 source_view
978 .index
979 .rows()
980 .filter(|row| source_view.includes(*row)),
981 ),
982 );
983 crate::profile::record_dataset_build(default_graph.len());
984 let graph_id = terms.intern(Term::NamedNode(
985 NamedNode::new(SHAPES_GRAPH_IRI).expect("static IRI is valid"),
986 ));
987 let named_graphs = HashMap::from([(graph_id, Arc::clone(&source.index))]);
988 if let Some(started) = started {
989 crate::profile::record_session_encode_time(started.elapsed().as_micros() as u64);
990 }
991 Self {
992 terms,
993 default_graph,
994 source_view: Some(source_view),
995 named_graphs,
996 reach_cache: RefCell::new(ReachCache::default()),
997 revision: 0,
998 stats,
999 }
1000 }
1001
1002 pub fn from_graph(graph: &Graph) -> Self {
1004 let terms = TermDictionary::new();
1005 let triples = intern_graph(graph, &terms);
1006 let default_graph = TripleIndex::build(triples);
1007 let stats = DatasetStatistics::compute(&default_graph);
1008 crate::profile::record_dataset_build(default_graph.len());
1009 Self {
1010 terms,
1011 default_graph,
1012 source_view: None,
1013 named_graphs: HashMap::new(),
1014 reach_cache: RefCell::new(ReachCache::default()),
1015 revision: 0,
1016 stats,
1017 }
1018 }
1019
1020 pub fn from_graph_union(left: &Graph, right: &Graph) -> Self {
1023 let terms = TermDictionary::new();
1024 let mut triples = intern_graph(left, &terms);
1025 triples.extend(intern_graph(right, &terms));
1026 let default_graph = TripleIndex::build(triples);
1027 let stats = DatasetStatistics::compute(&default_graph);
1028 crate::profile::record_dataset_build(default_graph.len());
1029 Self {
1030 terms,
1031 default_graph,
1032 source_view: None,
1033 named_graphs: HashMap::new(),
1034 reach_cache: RefCell::new(ReachCache::default()),
1035 revision: 0,
1036 stats,
1037 }
1038 }
1039
1040 pub(crate) fn intern(&self, term: &Term) -> TermId {
1044 self.terms.intern(term.clone())
1045 }
1046
1047 pub(crate) fn plan_stats(&self) -> shifty_opt::PlanStats {
1051 let predicate_cardinality: HashMap<Term, u64> = self
1052 .stats
1053 .predicate_cardinality
1054 .iter()
1055 .filter_map(|(&id, &count)| Some((self.externalize(id)?, count)))
1056 .collect();
1057 let distinct_predicates = predicate_cardinality.len() as u64;
1058 shifty_opt::PlanStats {
1059 total_triples: self.stats.triple_count,
1060 distinct_subjects: self.stats.distinct_subjects,
1061 distinct_objects: self.stats.distinct_objects,
1062 distinct_predicates,
1063 predicate_cardinality,
1064 }
1065 }
1066
1067 pub(crate) fn externalize(&self, id: TermId) -> Option<Term> {
1070 self.terms.externalize(id)
1071 }
1072
1073 pub(crate) fn externalize_id(&self, id: TermId) -> Term {
1077 self.terms
1078 .externalize(id)
1079 .expect("scanned TermId originates from this dataset")
1080 }
1081
1082 pub(crate) fn contains_ids(&self, subject: TermId, predicate: TermId, object: TermId) -> bool {
1083 self.default_graph.contains(subject, predicate, object)
1084 || self.source_view.as_ref().is_some_and(|source| {
1085 source.index.contains(subject, predicate, object)
1086 && source.includes([subject, predicate, object])
1087 })
1088 }
1089
1090 pub(crate) fn encode_triple(&self, triple: &oxrdf::Triple) -> [TermId; 3] {
1091 [
1092 self.terms.intern(triple.subject.clone().into()),
1093 self.terms.intern(Term::NamedNode(triple.predicate.clone())),
1094 self.terms.intern(triple.object.clone()),
1095 ]
1096 }
1097
1098 pub(crate) fn cached_reach(
1099 &self,
1100 node: TermId,
1101 step: &ReachStep,
1102 kind: ClosureKind,
1103 graph: GraphSel,
1104 ) -> Option<Rc<HashSet<TermId>>> {
1105 let result = self
1106 .reach_cache
1107 .borrow()
1108 .entries
1109 .get(&ReachCacheKey {
1110 node,
1111 step: step.clone(),
1112 kind,
1113 graph,
1114 })
1115 .cloned();
1116 crate::profile::record_reach_cache_lookup(result.is_some());
1117 result
1118 }
1119
1120 pub(crate) fn cache_reach(
1121 &self,
1122 node: TermId,
1123 step: &ReachStep,
1124 kind: ClosureKind,
1125 graph: GraphSel,
1126 result: Rc<HashSet<TermId>>,
1127 ) {
1128 let mut cache = self.reach_cache.borrow_mut();
1129 if cache.cached_ids.saturating_add(result.len()) > MAX_CACHED_REACH_IDS {
1133 crate::profile::record_reach_cache_admission(false, result.len());
1134 return;
1135 }
1136 let key = ReachCacheKey {
1137 node,
1138 step: step.clone(),
1139 kind,
1140 graph,
1141 };
1142 if cache.entries.contains_key(&key) {
1143 return;
1144 }
1145 cache.cached_ids += result.len();
1146 cache.entries.insert(key, result);
1147 crate::profile::record_reach_cache_admission(true, 0);
1148 }
1149
1150 pub(crate) fn scan(
1154 &self,
1155 s: Option<TermId>,
1156 p: Option<TermId>,
1157 o: Option<TermId>,
1158 graph: GraphSel,
1159 ) -> Box<dyn Iterator<Item = [TermId; 3]> + '_> {
1160 if let (GraphSel::Default, Some(s), Some(p), Some(o)) = (graph, s, p, o) {
1161 let found = self.contains_ids(s, p, o);
1162 crate::profile::record_scan("session", 7, usize::from(found));
1163 return Box::new(found.then_some([s, p, o]).into_iter());
1164 }
1165 let iter = match graph {
1166 GraphSel::Default => default_graph_quads(self, s, p, o),
1167 GraphSel::Named(g) => named_graph_quads(self, g, s, p, o),
1168 };
1169 Box::new(iter.map(|q| {
1170 let q = q.expect("infallible");
1171 [q.subject, q.predicate, q.object]
1172 }))
1173 }
1174
1175 pub(crate) fn scan_data(
1178 &self,
1179 s: Option<TermId>,
1180 p: Option<TermId>,
1181 o: Option<TermId>,
1182 ) -> Box<dyn Iterator<Item = [TermId; 3]> + '_> {
1183 let local = self.default_graph.scan(s, p, o);
1184 let Some(source) = &self.source_view else {
1185 return local;
1186 };
1187 Box::new(
1188 local.chain(
1189 source
1190 .index
1191 .scan(s, p, o)
1192 .filter(|row| source.data_member(*row)),
1193 ),
1194 )
1195 }
1196
1197 pub(crate) fn input_blank_nodes(&self) -> HashSet<BlankNode> {
1200 let mut nodes: HashSet<_> = self
1201 .scan_data(None, None, None)
1202 .flat_map(|[subject, _, object]| [subject, object])
1203 .filter_map(|id| self.terms.blank_node(id))
1204 .collect();
1205 if let Some(source) = &self.terms.source {
1206 nodes.extend(source.id_to_term.iter().filter_map(|term| match term {
1209 Term::BlankNode(node) => Some(node.clone()),
1210 _ => None,
1211 }));
1212 } else if let Some(graph) = self.terms.get(&Term::NamedNode(
1213 NamedNode::new(SHAPES_GRAPH_IRI).expect("static IRI is valid"),
1214 )) {
1215 nodes.extend(
1216 self.scan(None, None, None, GraphSel::Named(graph))
1217 .flat_map(|[subject, _, object]| [subject, object])
1218 .filter_map(|id| self.terms.blank_node(id)),
1219 );
1220 }
1221 nodes
1222 }
1223
1224 pub(crate) fn external_blank_node_map(&self) -> HashMap<BlankNode, BlankNode> {
1229 let aliases = self.terms.local_blank_aliases.borrow();
1230 let originals: HashSet<_> = aliases.keys().cloned().collect();
1231 let mut mapping = HashMap::with_capacity(aliases.len() * 2);
1232 let mut allocated = HashSet::with_capacity(aliases.len());
1233 for (original, alias) in aliases.iter() {
1234 let fresh = loop {
1235 let candidate = BlankNode::default();
1236 if self
1237 .terms
1238 .get(&Term::BlankNode(candidate.clone()))
1239 .is_none()
1240 && !originals.contains(&candidate)
1241 && !allocated.contains(&candidate)
1242 {
1243 break candidate;
1244 }
1245 };
1246 allocated.insert(fresh.clone());
1247 mapping.insert(original.clone(), fresh);
1248 mapping.insert(alias.clone(), original.clone());
1249 }
1250 mapping
1251 }
1252
1253 pub(crate) fn data_blank_node_aliases(&self) -> HashMap<BlankNode, BlankNode> {
1257 self.terms
1258 .local_blank_aliases
1259 .borrow()
1260 .iter()
1261 .map(|(original, alias)| (alias.clone(), original.clone()))
1262 .collect()
1263 }
1264
1265 pub(crate) fn data_graph_projection(&self) -> Graph {
1268 self.project_rows(self.scan_data(None, None, None))
1269 }
1270
1271 pub(crate) fn data_graph_projection_mapped(
1275 &self,
1276 blank_nodes: &HashMap<BlankNode, BlankNode>,
1277 ) -> Graph {
1278 self.project_rows_with(self.scan_data(None, None, None), |term| match term {
1279 Term::BlankNode(node) => {
1280 Term::BlankNode(blank_nodes.get(&node).cloned().unwrap_or(node))
1281 }
1282 other => other,
1283 })
1284 }
1285
1286 pub(crate) fn default_graph_projection(&self) -> Graph {
1288 self.project_rows(self.scan(None, None, None, GraphSel::Default))
1289 }
1290
1291 fn project_rows(&self, rows: impl Iterator<Item = [TermId; 3]>) -> Graph {
1292 self.project_rows_with(rows, |term| term)
1293 }
1294
1295 fn project_rows_with(
1296 &self,
1297 rows: impl Iterator<Item = [TermId; 3]>,
1298 map: impl Fn(Term) -> Term,
1299 ) -> Graph {
1300 let mut graph = Graph::new();
1301 for [subject, predicate, object] in rows {
1302 let subject: oxrdf::NamedOrBlankNode = match map(self.externalize_id(subject)) {
1303 Term::NamedNode(node) => node.into(),
1304 Term::BlankNode(node) => node.into(),
1305 Term::Literal(_) => unreachable!("RDF subjects cannot be literals"),
1306 };
1307 let Term::NamedNode(predicate) = self.externalize_id(predicate) else {
1308 unreachable!("RDF predicates are named nodes")
1309 };
1310 graph.insert(&oxrdf::Triple::new(
1311 subject,
1312 predicate,
1313 map(self.externalize_id(object)),
1314 ));
1315 }
1316 crate::profile::record_graph_projection(graph.len());
1317 graph
1318 }
1319
1320 pub(crate) fn triples_for_predicate(
1321 &self,
1322 predicate: &NamedNode,
1323 ) -> Box<dyn Iterator<Item = (Term, Term)> + '_> {
1324 let p = self.intern(&Term::NamedNode(predicate.clone()));
1325 Box::new(
1326 self.scan(None, Some(p), None, GraphSel::Default)
1327 .map(|[subject, _, object]| {
1328 (self.externalize_id(subject), self.externalize_id(object))
1329 }),
1330 )
1331 }
1332
1333 pub(crate) fn outgoing(
1334 &self,
1335 subject: &Term,
1336 ) -> Box<dyn Iterator<Item = (NamedNode, Term)> + '_> {
1337 let s = self.intern(subject);
1338 Box::new(
1339 self.scan(Some(s), None, None, GraphSel::Default)
1340 .filter_map(|[_, predicate, object]| {
1341 let Term::NamedNode(predicate) = self.externalize_id(predicate) else {
1342 return None;
1343 };
1344 Some((predicate, self.externalize_id(object)))
1345 }),
1346 )
1347 }
1348
1349 pub(crate) fn extend_triples<'a>(
1351 &mut self,
1352 triples: impl IntoIterator<Item = &'a oxrdf::Triple>,
1353 ) {
1354 let started = crate::profile::is_enabled().then(Instant::now);
1355 let mut encoded: Vec<_> = triples
1356 .into_iter()
1357 .map(|triple| self.encode_triple(triple))
1358 .collect();
1359 encoded.sort_unstable();
1360 encoded.dedup();
1361 encoded.retain(|&[s, p, o]| !self.contains_ids(s, p, o));
1362 let committed = encoded.len();
1363 self.stats.extend(&encoded);
1364 for &[_, predicate, _] in &encoded {
1365 if !self.default_graph.predicate_labels.contains_key(&predicate)
1366 && let Term::NamedNode(node) = self.externalize_id(predicate)
1367 {
1368 self.default_graph
1369 .predicate_labels
1370 .insert(predicate, node.as_str().to_owned());
1371 }
1372 }
1373 self.default_graph.extend(encoded);
1374 *self.reach_cache.borrow_mut() = ReachCache::default();
1375 if committed != 0 {
1376 self.revision = self
1377 .revision
1378 .checked_add(1)
1379 .expect("dataset revision overflowed u64");
1380 }
1381 if let Some(started) = started {
1382 crate::profile::record_dataset_commit(committed, started.elapsed().as_micros() as u64);
1383 }
1384 }
1385
1386 pub(crate) fn select_data_view(&mut self) {
1390 let Some(source) = &mut self.source_view else {
1391 return;
1392 };
1393 if !source.include_all {
1394 return;
1395 }
1396 source.include_all = false;
1397 self.stats = DatasetStatistics::compute_rows(
1398 self.default_graph
1399 .rows()
1400 .chain(source.index.rows().filter(|row| source.includes(*row))),
1401 );
1402 *self.reach_cache.borrow_mut() = ReachCache::default();
1403 self.revision = self
1404 .revision
1405 .checked_add(1)
1406 .expect("dataset revision overflowed u64");
1407 }
1408
1409 pub(crate) fn revision(&self) -> u64 {
1411 self.revision
1412 }
1413
1414 pub fn from_graphs(context: &Graph, shapes: &Graph) -> Self {
1418 let terms = TermDictionary::new();
1419 let triples = intern_graph(context, &terms);
1420 let default_graph = TripleIndex::build(triples);
1421 let stats = DatasetStatistics::compute(&default_graph);
1422 crate::profile::record_dataset_build(default_graph.len());
1423
1424 let shapes_iri = NamedNode::new(SHAPES_GRAPH_IRI).expect("static IRI is valid");
1425 let graph_id = terms.intern(Term::NamedNode(shapes_iri));
1426 let named_triples = Arc::new(TripleIndex::build(intern_graph(shapes, &terms)));
1427
1428 let mut named_graphs = HashMap::new();
1429 named_graphs.insert(graph_id, named_triples);
1430
1431 Self {
1432 terms,
1433 default_graph,
1434 source_view: None,
1435 named_graphs,
1436 reach_cache: RefCell::new(ReachCache::default()),
1437 revision: 0,
1438 stats,
1439 }
1440 }
1441
1442 pub fn from_graph_union_with_shapes(data: &Graph, shapes: &Graph) -> Self {
1445 let terms = TermDictionary::new();
1446 let mut triples = intern_graph(data, &terms);
1447 triples.extend(intern_graph(shapes, &terms));
1448 let default_graph = TripleIndex::build(triples);
1449 let stats = DatasetStatistics::compute(&default_graph);
1450 crate::profile::record_dataset_build(default_graph.len());
1451
1452 let shapes_iri = NamedNode::new(SHAPES_GRAPH_IRI).expect("static IRI is valid");
1453 let graph_id = terms.intern(Term::NamedNode(shapes_iri));
1454 let named_triples = Arc::new(TripleIndex::build(intern_graph(shapes, &terms)));
1455 let mut named_graphs = HashMap::new();
1456 named_graphs.insert(graph_id, named_triples);
1457
1458 Self {
1459 terms,
1460 default_graph,
1461 source_view: None,
1462 named_graphs,
1463 reach_cache: RefCell::new(ReachCache::default()),
1464 revision: 0,
1465 stats,
1466 }
1467 }
1468}
1469
1470fn intern_graph(graph: &Graph, terms: &TermDictionary) -> Vec<[TermId; 3]> {
1471 graph
1472 .iter()
1473 .map(|triple| {
1474 let s = terms.intern(triple.subject.into_owned().into());
1475 let p = terms.intern(Term::NamedNode(triple.predicate.into_owned()));
1476 let o = terms.intern(triple.object.into_owned());
1477 [s, p, o]
1478 })
1479 .collect()
1480}
1481
1482fn intern_data_graph(graph: &Graph, terms: &TermDictionary) -> Vec<[TermId; 3]> {
1483 graph
1484 .iter()
1485 .map(|triple| {
1486 let s = terms.intern_data(triple.subject.into_owned().into());
1487 let p = terms.intern(Term::NamedNode(triple.predicate.into_owned()));
1488 let o = terms.intern_data(triple.object.into_owned());
1489 [s, p, o]
1490 })
1491 .collect()
1492}
1493
1494fn predicate_labels(rows: &[[TermId; 3]], terms: &TermDictionary) -> HashMap<TermId, String> {
1495 rows.iter()
1496 .map(|row| row[1])
1497 .collect::<HashSet<_>>()
1498 .into_iter()
1499 .filter_map(|id| match terms.externalize(id) {
1500 Some(Term::NamedNode(node)) => Some((id, node.as_str().to_owned())),
1501 _ => None,
1502 })
1503 .collect()
1504}
1505
1506#[allow(refining_impl_trait)]
1509impl<'a> QueryableDataset<'a> for &'a FrozenIndexedDataset {
1510 type InternalTerm = TermId;
1511 type Error = Infallible;
1512
1513 fn internal_quads_for_pattern(
1514 &self,
1515 subject: Option<&TermId>,
1516 predicate: Option<&TermId>,
1517 object: Option<&TermId>,
1518 graph_name: Option<Option<&TermId>>,
1519 ) -> QuadIter<'a> {
1520 #[allow(clippy::explicit_auto_deref)]
1524 let ds: &'a FrozenIndexedDataset = *self;
1525 let s = subject.copied();
1526 let p = predicate.copied();
1527 let o = object.copied();
1528 match graph_name {
1529 Some(None) => default_graph_quads(ds, s, p, o),
1530 Some(Some(&g)) => named_graph_quads(ds, g, s, p, o),
1531 None => all_named_quads(ds, s, p, o),
1532 }
1533 }
1534
1535 fn internal_named_graphs(&self) -> impl Iterator<Item = Result<TermId, Infallible>> + use<'a> {
1536 self.named_graphs
1537 .keys()
1538 .copied()
1539 .map(Ok)
1540 .collect::<Vec<_>>()
1541 .into_iter()
1542 }
1543
1544 fn contains_internal_graph_name(&self, graph_name: &TermId) -> Result<bool, Infallible> {
1545 Ok(self.named_graphs.contains_key(graph_name))
1546 }
1547
1548 fn internalize_term(&self, term: Term) -> Result<TermId, Infallible> {
1549 Ok(self.terms.intern(term))
1553 }
1554
1555 fn externalize_term(&self, id: TermId) -> Result<Term, Infallible> {
1556 Ok(self
1557 .terms
1558 .externalize(id)
1559 .expect("TermId always originates from this dataset's internalize_term or internal_quads_for_pattern"))
1560 }
1561}
1562
1563type QuadIter<'a> = Box<dyn Iterator<Item = Result<InternalQuad<TermId>, Infallible>> + 'a>;
1566
1567fn mk_quad(s: TermId, p: TermId, o: TermId, g: Option<TermId>) -> InternalQuad<TermId> {
1568 InternalQuad {
1569 subject: s,
1570 predicate: p,
1571 object: o,
1572 graph_name: g,
1573 }
1574}
1575
1576fn default_graph_quads<'a>(
1578 ds: &'a FrozenIndexedDataset,
1579 s: Option<TermId>,
1580 p: Option<TermId>,
1581 o: Option<TermId>,
1582) -> QuadIter<'a> {
1583 let local = indexed_graph_quads(&ds.default_graph, None, s, p, o);
1584 let Some(source) = &ds.source_view else {
1585 return local;
1586 };
1587 let shared = indexed_graph_quads(&source.index, None, s, p, o).filter(move |quad| {
1591 let quad = quad.as_ref().expect("infallible RDF scan");
1592 let row = [quad.subject, quad.predicate, quad.object];
1593 source.includes(row)
1594 });
1595 Box::new(local.chain(shared))
1596}
1597
1598fn indexed_graph_quads<'a>(
1599 index: &'a TripleIndex,
1600 graph: Option<TermId>,
1601 s: Option<TermId>,
1602 p: Option<TermId>,
1603 o: Option<TermId>,
1604) -> QuadIter<'a> {
1605 Box::new(
1606 index
1607 .scan(s, p, o)
1608 .map(move |[s, p, o]| Ok(mk_quad(s, p, o, graph))),
1609 )
1610}
1611
1612fn named_graph_quads<'a>(
1616 ds: &'a FrozenIndexedDataset,
1617 g: TermId,
1618 s: Option<TermId>,
1619 p: Option<TermId>,
1620 o: Option<TermId>,
1621) -> QuadIter<'a> {
1622 let Some(index) = ds.named_graphs.get(&g) else {
1623 return Box::new(std::iter::empty());
1624 };
1625 indexed_graph_quads(index, Some(g), s, p, o)
1626}
1627
1628fn all_named_quads<'a>(
1631 ds: &'a FrozenIndexedDataset,
1632 s: Option<TermId>,
1633 p: Option<TermId>,
1634 o: Option<TermId>,
1635) -> QuadIter<'a> {
1636 Box::new(
1637 ds.named_graphs
1638 .keys()
1639 .copied()
1640 .flat_map(move |g| named_graph_quads(ds, g, s, p, o)),
1641 )
1642}
1643
1644#[cfg(test)]
1647mod tests {
1648 use super::*;
1649 use oxrdf::{NamedNode, Triple};
1650 use spareval::QueryableDataset;
1651
1652 fn nn(iri: &str) -> NamedNode {
1653 NamedNode::new(iri).unwrap()
1654 }
1655
1656 fn triple_nnn(s: &str, p: &str, o: &str) -> Triple {
1657 Triple::new(nn(s), nn(p), nn(o))
1658 }
1659
1660 fn small_graph() -> Graph {
1661 let mut g = Graph::new();
1662 g.insert(triple_nnn("http://ex/a", "http://ex/p", "http://ex/b").as_ref());
1663 g.insert(triple_nnn("http://ex/a", "http://ex/p", "http://ex/c").as_ref());
1664 g.insert(triple_nnn("http://ex/b", "http://ex/q", "http://ex/c").as_ref());
1665 g
1666 }
1667
1668 #[test]
1669 fn profile_accounts_for_source_index_reused_after_encoding() {
1670 let source = SourceStorage::encode(&small_graph());
1671 crate::profile::enable();
1672 let _dataset = FrozenIndexedDataset::from_data_with_source(
1673 &Graph::new(),
1674 Arc::clone(&source),
1675 false,
1676 false,
1677 );
1678 let profile = crate::profile::take().unwrap();
1679 assert_eq!(profile.storage().source_builds, 0);
1680 assert_eq!(
1681 profile.storage().source_primary_bytes as usize,
1682 source.index.primary_bytes()
1683 );
1684 }
1685
1686 #[test]
1687 fn empty_named_graph_remains_visible_to_sparql() {
1688 let source = SourceStorage::encode(&Graph::new());
1689 let dataset =
1690 FrozenIndexedDataset::from_data_with_source(&Graph::new(), source, false, false);
1691 let graph_id = dataset
1692 .terms
1693 .get(&Term::NamedNode(nn(SHAPES_GRAPH_IRI)))
1694 .unwrap();
1695 let queryable = &dataset;
1696
1697 assert_eq!(
1698 queryable
1699 .internal_named_graphs()
1700 .collect::<Result<Vec<_>, _>>()
1701 .unwrap(),
1702 vec![graph_id]
1703 );
1704 assert!(queryable.contains_internal_graph_name(&graph_id).unwrap());
1705 assert!(!queryable.contains_internal_graph_name(&u32::MAX).unwrap());
1706 assert!(
1707 queryable
1708 .internal_quads_for_pattern(None, None, None, Some(Some(&graph_id)))
1709 .next()
1710 .is_none()
1711 );
1712 }
1713
1714 #[test]
1715 fn reverse_index_policies_preserve_all_pattern_answers() {
1716 let rows: Vec<_> = (0..128).map(|s| [s, 9, s % 7]).collect();
1717 let base = TripleIndex::build_with_policy(
1718 rows.clone(),
1719 IndexPolicy::BaseOnly,
1720 0,
1721 HashSet::new(),
1722 false,
1723 false,
1724 );
1725 let all = TripleIndex::build_with_policy(
1726 rows.clone(),
1727 IndexPolicy::AllIndexes,
1728 usize::MAX,
1729 HashSet::new(),
1730 false,
1731 false,
1732 );
1733 let demand = TripleIndex::build_with_policy(
1734 rows.clone(),
1735 IndexPolicy::DemandDriven,
1736 0,
1737 HashSet::from([9]),
1738 false,
1739 false,
1740 );
1741 for (s, p, o) in [
1742 (None, None, None),
1743 (Some(3), None, None),
1744 (None, Some(9), None),
1745 (None, None, Some(2)),
1746 (Some(3), Some(9), None),
1747 (None, Some(9), Some(2)),
1748 (Some(3), None, Some(3)),
1749 (Some(3), Some(9), Some(3)),
1750 ] {
1751 let mut expected: Vec<_> = base.scan(s, p, o).collect();
1752 expected.sort_unstable();
1753 for index in [&all, &demand] {
1754 let mut got: Vec<_> = index.scan(s, p, o).collect();
1755 got.sort_unstable();
1756 assert_eq!(got, expected, "pattern {s:?} {p:?} {o:?}");
1757 }
1758 }
1759 assert_eq!(
1760 all.secondary_bytes.load(Ordering::Relaxed),
1761 128 * (8 + 12 + 12)
1762 );
1763 assert_eq!(demand.secondary_bytes.load(Ordering::Relaxed), 0);
1764 }
1765
1766 #[test]
1767 fn reverse_index_promotes_on_demand_and_merges_committed_rows() {
1768 let rows: Vec<_> = (0..128).map(|s| [s, 9, s % 7]).collect();
1769 let mut index = TripleIndex::build_with_policy(
1770 rows,
1771 IndexPolicy::DemandDriven,
1772 2048,
1773 HashSet::from([9]),
1774 false,
1775 false,
1776 );
1777 assert_eq!(index.scan(None, Some(9), Some(2)).count(), 18);
1778 assert!(index.partitions[&9].reverse.get().is_some());
1779 index.extend(vec![[200, 9, 2], [200, 9, 2], [201, 10, 2]]);
1780 assert!(index.contains(200, 9, 2));
1781 assert_eq!(index.scan(None, Some(9), Some(2)).count(), 19);
1782 assert_eq!(index.scan(None, Some(10), Some(2)).count(), 1);
1783 assert_eq!(index.len(), 130);
1784 }
1785
1786 #[test]
1787 fn general_directories_obey_budget_and_track_commits() {
1788 crate::profile::enable();
1789 let rows: Vec<_> = (0..300).map(|i| [i % 3, i + 10, i % 5]).collect();
1790 let mut index = TripleIndex::build_with_policy(
1791 rows.clone(),
1792 IndexPolicy::DemandDriven,
1793 300 * 12,
1794 HashSet::new(),
1795 true,
1796 true,
1797 );
1798 assert_eq!(index.scan(Some(1), None, None).count(), 100);
1799 assert!(index.subject_dir.get().is_some());
1800 assert_eq!(index.scan(None, None, Some(2)).count(), 60);
1803 assert!(matches!(index.object_dir.get(), Some(None)));
1804 index.extend(vec![[1, 900, 2]]);
1805 assert!(index.contains(1, 900, 2));
1806 assert_eq!(index.scan(Some(1), None, None).count(), 101);
1807 assert_eq!(index.scan(None, None, Some(2)).count(), 61);
1808 let profile = crate::profile::take().unwrap();
1809 assert_eq!(profile.indexes().len(), 3);
1810 assert_eq!(profile.indexes()[0].kind, "subject");
1811 assert!(profile.indexes()[0].accepted);
1812 assert_eq!(profile.indexes()[0].actual_bytes, 300 * 12);
1813 assert_eq!(profile.indexes()[1].kind, "object");
1814 assert!(!profile.indexes()[1].accepted);
1815 assert_eq!(profile.indexes()[1].budget_bytes, 300 * 12);
1816 assert_eq!(profile.indexes()[2].kind, "subject");
1817 assert!(!profile.indexes()[2].accepted);
1818 assert_eq!(profile.indexes()[2].estimated_bytes, 301 * 12);
1819 assert_eq!(
1820 profile.storage().session_primary_bytes as usize,
1821 index.primary_bytes()
1822 );
1823 }
1824
1825 #[test]
1826 fn source_index_profile_names_observed_predicate() {
1827 let mut graph = Graph::new();
1828 for index in 0..128 {
1829 graph.insert(&triple_nnn(
1830 &format!("http://ex/s{index}"),
1831 "http://ex/p",
1832 "http://ex/o",
1833 ));
1834 }
1835 crate::profile::enable();
1836 let source = SourceStorage::encode(&graph);
1837 let p = source.term_to_id[&Term::NamedNode(nn("http://ex/p"))];
1838 let o = source.term_to_id[&Term::NamedNode(nn("http://ex/o"))];
1839 for _ in 0..3 {
1840 assert_eq!(source.index.scan(None, Some(p), Some(o)).count(), 128);
1841 }
1842 let profile = crate::profile::take().unwrap();
1843 assert_eq!(profile.indexes().len(), 1);
1844 let index = &profile.indexes()[0];
1845 assert_eq!(index.scope, "source");
1846 assert_eq!(index.kind, "reverse");
1847 assert_eq!(index.predicate.as_deref(), Some("http://ex/p"));
1848 assert_eq!(index.reason, "observed probes");
1849 assert!(index.actual_bytes >= index.estimated_bytes);
1850 assert!(index.accepted);
1851 }
1852
1853 #[test]
1854 fn intern_round_trips() {
1855 let g = small_graph();
1856 let ds = FrozenIndexedDataset::from_graph(&g);
1857 let a = Term::NamedNode(nn("http://ex/a"));
1858 let id = ds.terms.intern(a.clone());
1859 assert_eq!(ds.terms.externalize(id), Some(a));
1860 }
1861
1862 #[test]
1863 fn contains_triple() {
1864 let g = small_graph();
1865 let ds = FrozenIndexedDataset::from_graph(&g);
1866 let s = ds.terms.get(&Term::NamedNode(nn("http://ex/a"))).unwrap();
1867 let p = ds.terms.get(&Term::NamedNode(nn("http://ex/p"))).unwrap();
1868 let o = ds.terms.get(&Term::NamedNode(nn("http://ex/b"))).unwrap();
1869 assert!(ds.default_graph.contains(s, p, o));
1870 }
1871
1872 #[test]
1873 fn missing_triple_not_found() {
1874 let g = small_graph();
1875 let ds = FrozenIndexedDataset::from_graph(&g);
1876 let s = ds.terms.get(&Term::NamedNode(nn("http://ex/a"))).unwrap();
1877 let p = ds.terms.get(&Term::NamedNode(nn("http://ex/q"))).unwrap();
1878 let o = ds.terms.get(&Term::NamedNode(nn("http://ex/c"))).unwrap();
1879 assert!(!ds.default_graph.contains(s, p, o));
1880 }
1881
1882 #[test]
1883 fn range_s_returns_correct_triples() {
1884 let g = small_graph();
1885 let ds = FrozenIndexedDataset::from_graph(&g);
1886 let s = ds.terms.get(&Term::NamedNode(nn("http://ex/a"))).unwrap();
1887 assert_eq!(ds.default_graph.scan(Some(s), None, None).count(), 2);
1888 }
1889
1890 #[test]
1891 fn extend_triples_updates_indexes_and_statistics() {
1892 let g = small_graph();
1893 let mut ds = FrozenIndexedDataset::from_graph(&g);
1894 let added = triple_nnn("http://ex/new", "http://ex/p", "http://ex/b");
1895 assert_eq!(ds.revision(), 0);
1896 ds.extend_triples([&added, &added]);
1897 assert_eq!(ds.revision(), 1);
1898 ds.extend_triples([&added]);
1899 assert_eq!(ds.revision(), 1, "a no-op batch is not a commit");
1900
1901 let s = ds.intern(&Term::NamedNode(nn("http://ex/new")));
1902 let p = ds.intern(&Term::NamedNode(nn("http://ex/p")));
1903 let o = ds.intern(&Term::NamedNode(nn("http://ex/b")));
1904 assert!(ds.contains_ids(s, p, o));
1905 assert_eq!(ds.stats.triple_count, 4);
1906 assert_eq!(ds.stats.predicate_cardinality[&p], 3);
1907 }
1908
1909 #[test]
1910 fn inferred_predicate_has_an_index_profile_label() {
1911 let mut ds = FrozenIndexedDataset::from_graph(&Graph::new());
1912 let predicate = nn("http://ex/inferred");
1913 let object = nn("http://ex/value");
1914 let triples: Vec<_> = (0..128)
1915 .map(|i| {
1916 Triple::new(
1917 nn(&format!("http://ex/s{i}")),
1918 predicate.clone(),
1919 object.clone(),
1920 )
1921 })
1922 .collect();
1923 ds.extend_triples(triples.iter());
1924 let p = ds.intern(&Term::NamedNode(predicate));
1925 let o = ds.intern(&Term::NamedNode(object));
1926 crate::profile::enable();
1927 for _ in 0..3 {
1928 assert_eq!(ds.default_graph.scan(None, Some(p), Some(o)).count(), 128);
1929 }
1930 let profile = crate::profile::take().unwrap();
1931 assert_eq!(
1932 profile.indexes()[0].predicate.as_deref(),
1933 Some("http://ex/inferred")
1934 );
1935 }
1936
1937 #[test]
1938 fn internalize_unknown_term_gets_unique_id() {
1939 let g = small_graph();
1940 let ds = FrozenIndexedDataset::from_graph(&g);
1941 let rds: &FrozenIndexedDataset = &ds;
1942 let unknown1 = Term::NamedNode(nn("http://ex/unknown1"));
1943 let unknown2 = Term::NamedNode(nn("http://ex/unknown2"));
1944 let id1 = rds.internalize_term(unknown1.clone()).unwrap();
1945 let id2 = rds.internalize_term(unknown2.clone()).unwrap();
1946 assert_ne!(id1, id2, "different unknown terms must get different IDs");
1947 let id1b = rds.internalize_term(unknown1).unwrap();
1949 assert_eq!(id1, id1b);
1950 }
1951
1952 #[test]
1953 fn queryable_dataset_default_graph_pattern() {
1954 let g = small_graph();
1955 let ds = FrozenIndexedDataset::from_graph(&g);
1956 let rds: &FrozenIndexedDataset = &ds;
1957 let quads: Vec<_> = rds
1959 .internal_quads_for_pattern(None, None, None, Some(None))
1960 .map(|r| r.unwrap())
1961 .collect();
1962 assert_eq!(quads.len(), 3);
1963 }
1964
1965 #[test]
1966 fn queryable_dataset_named_graph_empty_when_not_loaded() {
1967 let g = small_graph();
1968 let ds = FrozenIndexedDataset::from_graph(&g);
1969 let rds: &FrozenIndexedDataset = &ds;
1970 let quads: Vec<_> = rds
1972 .internal_quads_for_pattern(None, None, None, None)
1973 .collect();
1974 assert!(quads.is_empty());
1975 }
1976
1977 #[test]
1978 fn from_graphs_loads_both_default_and_named() {
1979 let data = small_graph();
1980 let mut shapes = Graph::new();
1981 shapes.insert(
1982 triple_nnn(
1983 "http://ex/S",
1984 "http://www.w3.org/ns/shacl#targetNode",
1985 "http://ex/a",
1986 )
1987 .as_ref(),
1988 );
1989 let ds = FrozenIndexedDataset::from_graphs(&data, &shapes);
1990 let rds: &FrozenIndexedDataset = &ds;
1991
1992 let default_quads: Vec<_> = rds
1994 .internal_quads_for_pattern(None, None, None, Some(None))
1995 .map(|r| r.unwrap())
1996 .collect();
1997 assert_eq!(default_quads.len(), 3);
1998
1999 let shapes_iri_id = ds
2001 .terms
2002 .get(&Term::NamedNode(nn(SHAPES_GRAPH_IRI)))
2003 .expect("shapes graph IRI should be interned");
2004 let named_quads: Vec<_> = rds
2005 .internal_quads_for_pattern(None, None, None, Some(Some(&shapes_iri_id)))
2006 .map(|r| r.unwrap())
2007 .collect();
2008 assert_eq!(named_quads.len(), 1);
2009 }
2010
2011 #[test]
2012 fn named_graph_bound_subject_and_predicate_ranges_preserve_answers() {
2013 let data = Graph::new();
2014 let mut shapes = Graph::new();
2015 for triple in [
2016 triple_nnn("http://ex/a", "http://ex/p", "http://ex/one"),
2017 triple_nnn("http://ex/a", "http://ex/p", "http://ex/two"),
2018 triple_nnn("http://ex/a", "http://ex/q", "http://ex/three"),
2019 triple_nnn("http://ex/b", "http://ex/p", "http://ex/four"),
2020 ] {
2021 shapes.insert(&triple);
2022 }
2023 let ds = FrozenIndexedDataset::from_graphs(&data, &shapes);
2024 let a = ds.intern(&Term::NamedNode(nn("http://ex/a")));
2025 let p = ds.intern(&Term::NamedNode(nn("http://ex/p")));
2026 let g = ds.intern(&Term::NamedNode(nn(SHAPES_GRAPH_IRI)));
2027 assert_eq!(ds.scan(Some(a), None, None, GraphSel::Named(g)).count(), 3);
2028 assert_eq!(
2029 ds.scan(Some(a), Some(p), None, GraphSel::Named(g)).count(),
2030 2
2031 );
2032 assert_eq!(
2033 ds.scan(
2034 Some(a),
2035 Some(p),
2036 Some(ds.intern(&Term::NamedNode(nn("http://ex/two")))),
2037 GraphSel::Named(g)
2038 )
2039 .count(),
2040 1
2041 );
2042 }
2043
2044 #[test]
2045 fn source_ids_and_named_index_are_shared_without_restoring_embedded_deletions() {
2046 let mut shapes = Graph::new();
2047 let overlap = triple_nnn("http://ex/a", "http://ex/p", "http://ex/b");
2048 shapes.insert(&overlap);
2049 let source = SourceStorage::encode(&shapes);
2050 let empty = Graph::new();
2051 let separate =
2052 FrozenIndexedDataset::from_data_with_source(&empty, Arc::clone(&source), true, true);
2053 let embedded_after_delete =
2054 FrozenIndexedDataset::from_data_with_source(&empty, Arc::clone(&source), false, false);
2055 let s = separate.intern(&Term::NamedNode(nn("http://ex/a")));
2056 let p = separate.intern(&Term::NamedNode(nn("http://ex/p")));
2057 let o = separate.intern(&Term::NamedNode(nn("http://ex/b")));
2058 let g = separate.intern(&Term::NamedNode(nn(SHAPES_GRAPH_IRI)));
2059 assert_eq!(
2060 s,
2061 embedded_after_delete.intern(&Term::NamedNode(nn("http://ex/a")))
2062 );
2063 assert!(separate.contains_ids(s, p, o));
2064 assert!(!embedded_after_delete.contains_ids(s, p, o));
2065 assert_eq!(separate.stats.triple_count, 1);
2066 assert_eq!(embedded_after_delete.stats.triple_count, 0);
2067 assert_eq!(separate.default_graph.len(), 0);
2068 assert_eq!(
2069 separate.scan(None, None, None, GraphSel::Default).count(),
2070 1
2071 );
2072 assert_eq!(
2073 embedded_after_delete
2074 .scan(None, None, None, GraphSel::Default)
2075 .count(),
2076 0
2077 );
2078 assert_eq!(
2079 embedded_after_delete
2080 .scan(Some(s), Some(p), Some(o), GraphSel::Named(g))
2081 .count(),
2082 1
2083 );
2084 assert!(Arc::ptr_eq(
2085 separate.named_graphs.get(&g).unwrap(),
2086 embedded_after_delete.named_graphs.get(&g).unwrap(),
2087 ));
2088 }
2089
2090 #[test]
2091 fn separate_overlap_has_two_memberships_but_one_union_result() {
2092 let mut shapes = Graph::new();
2093 let overlap = triple_nnn("http://ex/a", "http://ex/p", "http://ex/b");
2094 shapes.insert(&overlap);
2095 shapes.insert(&triple_nnn(
2096 "http://ex/a",
2097 "http://ex/q",
2098 "http://ex/source",
2099 ));
2100 let mut data = Graph::new();
2101 data.insert(&overlap);
2102 data.insert(&triple_nnn("http://ex/a", "http://ex/r", "http://ex/local"));
2103 let source = SourceStorage::encode(&shapes);
2104 let mut union =
2105 FrozenIndexedDataset::from_data_with_source(&data, Arc::clone(&source), true, true);
2106 let data_only = FrozenIndexedDataset::from_data_with_source(&data, source, true, false);
2107 let g = union.intern(&Term::NamedNode(nn(SHAPES_GRAPH_IRI)));
2108 assert_eq!(union.scan(None, None, None, GraphSel::Default).count(), 3);
2109 assert_eq!(
2110 data_only.scan(None, None, None, GraphSel::Default).count(),
2111 2
2112 );
2113 assert_eq!(union.scan(None, None, None, GraphSel::Named(g)).count(), 2);
2114 assert_eq!(union.stats.triple_count, 3);
2115 assert_eq!(data_only.stats.triple_count, 2);
2116 union.select_data_view();
2117 assert_eq!(
2118 union
2119 .scan(None, None, None, GraphSel::Default)
2120 .collect::<HashSet<_>>(),
2121 data_only
2122 .scan(None, None, None, GraphSel::Default)
2123 .collect::<HashSet<_>>()
2124 );
2125 assert_eq!(union.scan(None, None, None, GraphSel::Named(g)).count(), 2);
2126 assert_eq!(union.stats.triple_count, 2);
2127 }
2128
2129 #[test]
2130 fn separately_loaded_blank_node_labels_keep_distinct_identity() {
2131 let shapes = shifty_parse::load_turtle(
2132 br#"@prefix ex: <http://ex/> . _:same ex:p ex:value ."#,
2133 None,
2134 )
2135 .unwrap();
2136 let data = shifty_parse::load_turtle(
2137 br#"@prefix ex: <http://ex/> . _:same ex:p ex:value ."#,
2138 None,
2139 )
2140 .unwrap();
2141 let source = SourceStorage::encode(&shapes.graph);
2142 let dataset = FrozenIndexedDataset::from_data_with_source(&data.graph, source, true, true);
2143 let subjects: HashSet<_> = dataset
2144 .scan(None, None, None, GraphSel::Default)
2145 .map(|[subject, _, _]| subject)
2146 .collect();
2147 assert_eq!(dataset.stats.triple_count, 2);
2148 assert_eq!(subjects.len(), 2);
2149 }
2150
2151 #[test]
2152 fn embedded_source_membership_uses_exclusions_after_deletion() {
2153 let mut source_graph = Graph::new();
2154 let first = triple_nnn("http://ex/a", "http://ex/p", "http://ex/one");
2155 let second = triple_nnn("http://ex/a", "http://ex/p", "http://ex/two");
2156 source_graph.insert(&first);
2157 source_graph.insert(&second);
2158 let source = SourceStorage::encode(&source_graph);
2159 let full = FrozenIndexedDataset::from_data_with_source(
2160 &source_graph,
2161 Arc::clone(&source),
2162 false,
2163 false,
2164 );
2165 assert!(matches!(
2166 full.source_view.as_ref().unwrap().membership,
2167 SourceMembership::All
2168 ));
2169 let mut edited = source_graph.clone();
2170 edited.remove(&second);
2171 let after_delete =
2172 FrozenIndexedDataset::from_data_with_source(&edited, source, false, false);
2173 assert_eq!(
2174 after_delete
2175 .scan(None, None, None, GraphSel::Default)
2176 .count(),
2177 1
2178 );
2179 assert_eq!(after_delete.stats.triple_count, 1);
2180 assert_eq!(after_delete.default_graph.len(), 0);
2181 }
2182
2183 #[test]
2184 fn graph_union_builds_one_deduplicated_default_graph() {
2185 let left = small_graph();
2186 let mut right = Graph::new();
2187 right.insert(triple_nnn("http://ex/a", "http://ex/p", "http://ex/b").as_ref());
2188 right.insert(triple_nnn("http://ex/new", "http://ex/p", "http://ex/b").as_ref());
2189
2190 let ds = FrozenIndexedDataset::from_graph_union(&left, &right);
2191
2192 assert_eq!(ds.stats.triple_count, 4);
2193 }
2194}