1use std::collections::HashMap;
10
11#[derive(Debug, Clone, Default)]
20pub struct GraphStatistics {
21 total_triples: u64,
22 distinct_subjects: u64,
23 distinct_predicates: u64,
24 distinct_objects: u64,
25 predicate_frequency: HashMap<String, u64>,
26 subject_frequency: HashMap<String, u64>,
27 object_frequency: HashMap<String, u64>,
28}
29
30impl GraphStatistics {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn add_triple(&mut self, subject: &str, predicate: &str, object: &str) {
38 self.total_triples += 1;
39 let s_prev = *self.subject_frequency.get(subject).unwrap_or(&0);
40 if s_prev == 0 {
41 self.distinct_subjects += 1;
42 }
43 *self
44 .subject_frequency
45 .entry(subject.to_string())
46 .or_insert(0) += 1;
47
48 let p_prev = *self.predicate_frequency.get(predicate).unwrap_or(&0);
49 if p_prev == 0 {
50 self.distinct_predicates += 1;
51 }
52 *self
53 .predicate_frequency
54 .entry(predicate.to_string())
55 .or_insert(0) += 1;
56
57 let o_prev = *self.object_frequency.get(object).unwrap_or(&0);
58 if o_prev == 0 {
59 self.distinct_objects += 1;
60 }
61 *self.object_frequency.entry(object.to_string()).or_insert(0) += 1;
62 }
63
64 pub fn remove_triple(&mut self, subject: &str, predicate: &str, object: &str) {
66 if self.total_triples == 0 {
67 return;
68 }
69 self.total_triples = self.total_triples.saturating_sub(1);
70
71 if let Some(cnt) = self.subject_frequency.get_mut(subject) {
72 *cnt = cnt.saturating_sub(1);
73 if *cnt == 0 {
74 self.subject_frequency.remove(subject);
75 self.distinct_subjects = self.distinct_subjects.saturating_sub(1);
76 }
77 }
78
79 if let Some(cnt) = self.predicate_frequency.get_mut(predicate) {
80 *cnt = cnt.saturating_sub(1);
81 if *cnt == 0 {
82 self.predicate_frequency.remove(predicate);
83 self.distinct_predicates = self.distinct_predicates.saturating_sub(1);
84 }
85 }
86
87 if let Some(cnt) = self.object_frequency.get_mut(object) {
88 *cnt = cnt.saturating_sub(1);
89 if *cnt == 0 {
90 self.object_frequency.remove(object);
91 self.distinct_objects = self.distinct_objects.saturating_sub(1);
92 }
93 }
94 }
95
96 pub fn total_triples(&self) -> u64 {
98 self.total_triples
99 }
100
101 pub fn distinct_subjects(&self) -> u64 {
103 self.distinct_subjects
104 }
105
106 pub fn distinct_predicates(&self) -> u64 {
108 self.distinct_predicates
109 }
110
111 pub fn distinct_objects(&self) -> u64 {
113 self.distinct_objects
114 }
115
116 pub fn predicate_frequency(&self, predicate: &str) -> u64 {
118 *self.predicate_frequency.get(predicate).unwrap_or(&0)
119 }
120
121 pub fn top_predicates(&self, limit: usize) -> Vec<(String, u64)> {
123 let mut v: Vec<_> = self
124 .predicate_frequency
125 .iter()
126 .map(|(k, &v)| (k.clone(), v))
127 .collect();
128 v.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
129 v.truncate(limit);
130 v
131 }
132
133 pub fn estimate_cardinality(
138 &self,
139 subject: Option<&str>,
140 predicate: Option<&str>,
141 object: Option<&str>,
142 ) -> u64 {
143 if self.total_triples == 0 {
144 return 0;
145 }
146 let n = self.total_triples as f64;
147
148 let s_sel = match subject {
149 None => 1.0,
150 Some(s) => {
151 let freq = *self.subject_frequency.get(s).unwrap_or(&0) as f64;
152 if freq == 0.0 {
153 0.0
154 } else {
155 freq / n
156 }
157 }
158 };
159
160 let p_sel = match predicate {
161 None => 1.0,
162 Some(p) => {
163 let freq = *self.predicate_frequency.get(p).unwrap_or(&0) as f64;
164 if freq == 0.0 {
165 0.0
166 } else {
167 freq / n
168 }
169 }
170 };
171
172 let o_sel = match object {
173 None => 1.0,
174 Some(o) => {
175 let freq = *self.object_frequency.get(o).unwrap_or(&0) as f64;
176 if freq == 0.0 {
177 0.0
178 } else {
179 freq / n
180 }
181 }
182 };
183
184 (n * s_sel * p_sel * o_sel).ceil() as u64
185 }
186
187 pub fn merge(&mut self, other: &GraphStatistics) {
189 self.total_triples += other.total_triples;
190 for (s, &cnt) in &other.subject_frequency {
191 let prev = *self.subject_frequency.get(s).unwrap_or(&0);
192 if prev == 0 {
193 self.distinct_subjects += 1;
194 }
195 *self.subject_frequency.entry(s.clone()).or_insert(0) += cnt;
196 }
197 for (p, &cnt) in &other.predicate_frequency {
198 let prev = *self.predicate_frequency.get(p).unwrap_or(&0);
199 if prev == 0 {
200 self.distinct_predicates += 1;
201 }
202 *self.predicate_frequency.entry(p.clone()).or_insert(0) += cnt;
203 }
204 for (o, &cnt) in &other.object_frequency {
205 let prev = *self.object_frequency.get(o).unwrap_or(&0);
206 if prev == 0 {
207 self.distinct_objects += 1;
208 }
209 *self.object_frequency.entry(o.clone()).or_insert(0) += cnt;
210 }
211 }
212}
213
214#[derive(Debug, Clone, Default)]
222pub struct PredicateHistogram {
223 frequencies: HashMap<String, u64>,
224 total: u64,
225}
226
227impl PredicateHistogram {
228 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn record(&mut self, predicate: &str) {
234 *self.frequencies.entry(predicate.to_string()).or_insert(0) += 1;
235 self.total += 1;
236 }
237
238 pub fn remove(&mut self, predicate: &str) {
240 if let Some(cnt) = self.frequencies.get_mut(predicate) {
241 if *cnt > 0 {
242 *cnt -= 1;
243 self.total = self.total.saturating_sub(1);
244 if *cnt == 0 {
245 self.frequencies.remove(predicate);
246 }
247 }
248 }
249 }
250
251 pub fn frequency(&self, predicate: &str) -> u64 {
253 *self.frequencies.get(predicate).unwrap_or(&0)
254 }
255
256 pub fn relative_frequency(&self, predicate: &str) -> f64 {
258 if self.total == 0 {
259 0.0
260 } else {
261 *self.frequencies.get(predicate).unwrap_or(&0) as f64 / self.total as f64
262 }
263 }
264
265 pub fn total(&self) -> u64 {
267 self.total
268 }
269
270 pub fn distinct_count(&self) -> usize {
272 self.frequencies.len()
273 }
274
275 pub fn top_n(&self, n: usize) -> Vec<(String, u64)> {
277 let mut v: Vec<_> = self
278 .frequencies
279 .iter()
280 .map(|(k, &v)| (k.clone(), v))
281 .collect();
282 v.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
283 v.truncate(n);
284 v
285 }
286
287 pub fn bottom_n(&self, n: usize) -> Vec<(String, u64)> {
289 let mut v: Vec<_> = self
290 .frequencies
291 .iter()
292 .map(|(k, &v)| (k.clone(), v))
293 .collect();
294 v.sort_by_key(|(_, count)| *count);
295 v.truncate(n);
296 v
297 }
298
299 pub fn all_sorted(&self) -> Vec<(String, u64)> {
301 let mut v: Vec<_> = self
302 .frequencies
303 .iter()
304 .map(|(k, &v)| (k.clone(), v))
305 .collect();
306 v.sort_by(|a, b| a.0.cmp(&b.0));
307 v
308 }
309}
310
311#[derive(Debug, Clone, Default)]
320pub struct CardinalityEstimator {
321 total_triples: u64,
322 distinct_subjects: u64,
323 distinct_objects: u64,
324 predicate_freq: HashMap<String, u64>,
325}
326
327impl CardinalityEstimator {
328 pub fn new() -> Self {
329 Self::default()
330 }
331
332 pub fn from_graph_stats(stats: &GraphStatistics) -> Self {
334 Self {
335 total_triples: stats.total_triples(),
336 distinct_subjects: stats.distinct_subjects(),
337 distinct_objects: stats.distinct_objects(),
338 predicate_freq: stats.predicate_frequency.clone(),
339 }
340 }
341
342 pub fn estimate_pattern(
349 &self,
350 subject_bound: bool,
351 predicate: Option<&str>,
352 object_bound: bool,
353 ) -> u64 {
354 if self.total_triples == 0 {
355 return 0;
356 }
357 let n = self.total_triples as f64;
358 let mut sel = 1.0_f64;
359
360 if subject_bound {
361 let ds = self.distinct_subjects.max(1) as f64;
362 sel *= 1.0 / ds;
363 }
364
365 if let Some(p) = predicate {
366 let freq = *self.predicate_freq.get(p).unwrap_or(&0) as f64;
367 if freq == 0.0 {
368 return 1; }
370 sel *= freq / n;
371 }
372
373 if object_bound {
374 let dobj = self.distinct_objects.max(1) as f64;
375 sel *= 1.0 / dobj;
376 }
377
378 (n * sel).ceil() as u64
379 }
380
381 pub fn estimate_join(&self, left_card: u64, right_card: u64, shared_vars: usize) -> u64 {
383 if self.total_triples == 0 || left_card == 0 || right_card == 0 {
384 return 0;
385 }
386 let n = self.total_triples.max(1) as f64;
387 let reduction = n.powi(shared_vars as i32);
389 ((left_card as f64 * right_card as f64 / reduction).ceil() as u64).max(1)
390 }
391
392 pub fn update_from(&mut self, stats: &GraphStatistics) {
394 self.total_triples = stats.total_triples();
395 self.distinct_subjects = stats.distinct_subjects();
396 self.distinct_objects = stats.distinct_objects();
397 self.predicate_freq = stats.predicate_frequency.clone();
398 }
399}
400
401#[derive(Debug, Clone)]
411pub struct SampledStatistics {
412 sample_size: usize,
413 sample: Vec<(String, String, String)>,
414 total_seen: u64,
415 graph_stats: GraphStatistics,
416}
417
418impl SampledStatistics {
419 pub fn new(sample_size: usize) -> Self {
421 Self {
422 sample_size,
423 sample: Vec::with_capacity(sample_size),
424 total_seen: 0,
425 graph_stats: GraphStatistics::new(),
426 }
427 }
428
429 pub fn observe(&mut self, subject: &str, predicate: &str, object: &str) {
434 self.total_seen += 1;
435 self.graph_stats.add_triple(subject, predicate, object);
436
437 if self.sample.len() < self.sample_size {
438 self.sample.push((
439 subject.to_string(),
440 predicate.to_string(),
441 object.to_string(),
442 ));
443 } else {
444 let hash = Self::hash3(subject, predicate, object, self.total_seen);
446 let idx = (hash % self.total_seen) as usize;
447 if idx < self.sample_size {
448 self.sample[idx] = (
449 subject.to_string(),
450 predicate.to_string(),
451 object.to_string(),
452 );
453 }
454 }
455 }
456
457 fn hash3(s: &str, p: &str, o: &str, n: u64) -> u64 {
458 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
459 const FNV_PRIME: u64 = 1_099_511_628_211;
460 let mut h = FNV_OFFSET;
461 for b in s.bytes().chain(p.bytes()).chain(o.bytes()) {
462 h ^= b as u64;
463 h = h.wrapping_mul(FNV_PRIME);
464 }
465 h ^= n;
466 h = h.wrapping_mul(FNV_PRIME);
467 h
468 }
469
470 pub fn total_seen(&self) -> u64 {
472 self.total_seen
473 }
474
475 pub fn sample_size(&self) -> usize {
477 self.sample.len()
478 }
479
480 pub fn sample(&self) -> &[(String, String, String)] {
482 &self.sample
483 }
484
485 pub fn estimated_total(&self) -> u64 {
487 self.total_seen
488 }
489
490 pub fn estimated_distinct_predicates(&self) -> u64 {
492 let sample_distinct = self.graph_stats.distinct_predicates();
493 if self.sample.is_empty() || self.total_seen == 0 {
494 return sample_distinct;
495 }
496 let ratio = self.total_seen as f64 / self.sample.len() as f64;
498 (sample_distinct as f64 * ratio.sqrt()).ceil() as u64
499 }
500
501 pub fn estimate_cardinality(
503 &self,
504 subject: Option<&str>,
505 predicate: Option<&str>,
506 object: Option<&str>,
507 ) -> u64 {
508 self.graph_stats
509 .estimate_cardinality(subject, predicate, object)
510 }
511
512 pub fn graph_stats(&self) -> &GraphStatistics {
514 &self.graph_stats
515 }
516}
517
518#[cfg(test)]
523mod tests {
524 use super::*;
525
526 #[test]
529 fn test_graph_stats_empty() {
530 let gs = GraphStatistics::new();
531 assert_eq!(gs.total_triples(), 0);
532 assert_eq!(gs.distinct_subjects(), 0);
533 assert_eq!(gs.distinct_predicates(), 0);
534 assert_eq!(gs.distinct_objects(), 0);
535 }
536
537 #[test]
538 fn test_graph_stats_add_single_triple() {
539 let mut gs = GraphStatistics::new();
540 gs.add_triple("alice", "knows", "bob");
541 assert_eq!(gs.total_triples(), 1);
542 assert_eq!(gs.distinct_subjects(), 1);
543 assert_eq!(gs.distinct_predicates(), 1);
544 assert_eq!(gs.distinct_objects(), 1);
545 }
546
547 #[test]
548 fn test_graph_stats_add_multiple_triples_same_predicate() {
549 let mut gs = GraphStatistics::new();
550 gs.add_triple("alice", "knows", "bob");
551 gs.add_triple("alice", "knows", "carol");
552 gs.add_triple("bob", "knows", "carol");
553 assert_eq!(gs.total_triples(), 3);
554 assert_eq!(gs.distinct_predicates(), 1);
555 assert_eq!(gs.predicate_frequency("knows"), 3);
556 }
557
558 #[test]
559 fn test_graph_stats_distinct_counts_correct() {
560 let mut gs = GraphStatistics::new();
561 gs.add_triple("s1", "p1", "o1");
562 gs.add_triple("s1", "p2", "o2");
563 gs.add_triple("s2", "p1", "o1");
564 assert_eq!(gs.distinct_subjects(), 2);
565 assert_eq!(gs.distinct_predicates(), 2);
566 assert_eq!(gs.distinct_objects(), 2);
567 }
568
569 #[test]
570 fn test_graph_stats_remove_triple() {
571 let mut gs = GraphStatistics::new();
572 gs.add_triple("s", "p", "o");
573 gs.remove_triple("s", "p", "o");
574 assert_eq!(gs.total_triples(), 0);
575 assert_eq!(gs.distinct_subjects(), 0);
576 assert_eq!(gs.distinct_predicates(), 0);
577 assert_eq!(gs.distinct_objects(), 0);
578 }
579
580 #[test]
581 fn test_graph_stats_remove_partial() {
582 let mut gs = GraphStatistics::new();
583 gs.add_triple("s", "p", "o1");
584 gs.add_triple("s", "p", "o2");
585 gs.remove_triple("s", "p", "o1");
586 assert_eq!(gs.total_triples(), 1);
587 assert_eq!(gs.distinct_subjects(), 1); assert_eq!(gs.distinct_objects(), 1); }
590
591 #[test]
592 fn test_graph_stats_remove_nonexistent_is_noop() {
593 let mut gs = GraphStatistics::new();
594 gs.remove_triple("nonexistent", "p", "o"); assert_eq!(gs.total_triples(), 0);
596 }
597
598 #[test]
599 fn test_graph_stats_estimate_cardinality_wildcard() {
600 let mut gs = GraphStatistics::new();
601 for i in 0..100 {
602 gs.add_triple(&format!("s{i}"), "p", "o");
603 }
604 let card = gs.estimate_cardinality(None, None, None);
605 assert_eq!(card, 100);
606 }
607
608 #[test]
609 fn test_graph_stats_estimate_cardinality_bound_predicate() {
610 let mut gs = GraphStatistics::new();
611 for i in 0..100 {
612 gs.add_triple(&format!("s{i}"), "p1", "o");
613 gs.add_triple(&format!("s{i}"), "p2", "o");
614 }
615 let card = gs.estimate_cardinality(None, Some("p1"), None);
616 assert!(card <= 100);
617 assert!(card >= 1);
618 }
619
620 #[test]
621 fn test_graph_stats_estimate_cardinality_unknown_predicate() {
622 let mut gs = GraphStatistics::new();
623 gs.add_triple("s", "p", "o");
624 let card = gs.estimate_cardinality(None, Some("nonexistent"), None);
625 assert_eq!(card, 0);
626 }
627
628 #[test]
629 fn test_graph_stats_top_predicates() {
630 let mut gs = GraphStatistics::new();
631 for _ in 0..10 {
632 gs.add_triple("s", "popular", "o");
633 }
634 for _ in 0..2 {
635 gs.add_triple("s", "rare", "o");
636 }
637 let top = gs.top_predicates(1);
638 assert_eq!(top[0].0, "popular");
639 assert_eq!(top[0].1, 10);
640 }
641
642 #[test]
643 fn test_graph_stats_merge() {
644 let mut gs1 = GraphStatistics::new();
645 gs1.add_triple("s1", "p", "o1");
646 let mut gs2 = GraphStatistics::new();
647 gs2.add_triple("s2", "p", "o2");
648 gs1.merge(&gs2);
649 assert_eq!(gs1.total_triples(), 2);
650 assert_eq!(gs1.distinct_subjects(), 2);
651 }
652
653 #[test]
656 fn test_predicate_histogram_empty() {
657 let ph = PredicateHistogram::new();
658 assert_eq!(ph.total(), 0);
659 assert_eq!(ph.distinct_count(), 0);
660 assert_eq!(ph.frequency("any"), 0);
661 }
662
663 #[test]
664 fn test_predicate_histogram_record() {
665 let mut ph = PredicateHistogram::new();
666 ph.record("p1");
667 ph.record("p1");
668 ph.record("p2");
669 assert_eq!(ph.total(), 3);
670 assert_eq!(ph.frequency("p1"), 2);
671 assert_eq!(ph.frequency("p2"), 1);
672 assert_eq!(ph.distinct_count(), 2);
673 }
674
675 #[test]
676 fn test_predicate_histogram_relative_frequency() {
677 let mut ph = PredicateHistogram::new();
678 for _ in 0..3 {
679 ph.record("a");
680 }
681 for _ in 0..7 {
682 ph.record("b");
683 }
684 let rf_a = ph.relative_frequency("a");
685 let rf_b = ph.relative_frequency("b");
686 assert!((rf_a - 0.3).abs() < 1e-9);
687 assert!((rf_b - 0.7).abs() < 1e-9);
688 }
689
690 #[test]
691 fn test_predicate_histogram_remove() {
692 let mut ph = PredicateHistogram::new();
693 ph.record("p");
694 ph.record("p");
695 ph.remove("p");
696 assert_eq!(ph.frequency("p"), 1);
697 ph.remove("p");
698 assert_eq!(ph.frequency("p"), 0);
699 assert_eq!(ph.distinct_count(), 0);
700 }
701
702 #[test]
703 fn test_predicate_histogram_top_n() {
704 let mut ph = PredicateHistogram::new();
705 for i in 0..5 {
706 for _ in 0..(i + 1) {
707 ph.record(&format!("p{i}"));
708 }
709 }
710 let top = ph.top_n(2);
711 assert_eq!(top.len(), 2);
712 assert!(top[0].1 >= top[1].1);
713 }
714
715 #[test]
716 fn test_predicate_histogram_bottom_n() {
717 let mut ph = PredicateHistogram::new();
718 for _ in 0..10 {
719 ph.record("common");
720 }
721 for _ in 0..1 {
722 ph.record("rare");
723 }
724 let bottom = ph.bottom_n(1);
725 assert_eq!(bottom[0].0, "rare");
726 }
727
728 #[test]
729 fn test_predicate_histogram_all_sorted() {
730 let mut ph = PredicateHistogram::new();
731 ph.record("zebra");
732 ph.record("apple");
733 ph.record("mango");
734 let sorted = ph.all_sorted();
735 assert_eq!(sorted[0].0, "apple");
736 assert_eq!(sorted[1].0, "mango");
737 assert_eq!(sorted[2].0, "zebra");
738 }
739
740 #[test]
743 fn test_cardinality_estimator_empty() {
744 let ce = CardinalityEstimator::new();
745 assert_eq!(ce.estimate_pattern(false, None, false), 0);
746 }
747
748 #[test]
749 fn test_cardinality_estimator_from_graph_stats() {
750 let mut gs = GraphStatistics::new();
751 for i in 0..50 {
752 gs.add_triple(&format!("s{i}"), "knows", "o");
753 }
754 let ce = CardinalityEstimator::from_graph_stats(&gs);
755 let card = ce.estimate_pattern(false, Some("knows"), false);
757 assert_eq!(card, 50);
758 }
759
760 #[test]
761 fn test_cardinality_estimator_bound_subject_reduces_card() {
762 let mut gs = GraphStatistics::new();
763 for i in 0..100 {
764 gs.add_triple(&format!("s{i}"), "p", "o");
765 }
766 let ce = CardinalityEstimator::from_graph_stats(&gs);
767 let card_with = ce.estimate_pattern(true, None, false);
768 let card_without = ce.estimate_pattern(false, None, false);
769 assert!(card_with <= card_without);
770 }
771
772 #[test]
773 fn test_cardinality_estimator_unknown_predicate() {
774 let mut gs = GraphStatistics::new();
775 gs.add_triple("s", "known_p", "o");
776 let ce = CardinalityEstimator::from_graph_stats(&gs);
777 let card = ce.estimate_pattern(false, Some("unknown_p"), false);
778 assert_eq!(card, 1); }
780
781 #[test]
782 fn test_cardinality_estimator_estimate_join() {
783 let mut gs = GraphStatistics::new();
784 for i in 0..100 {
785 gs.add_triple(&format!("s{i}"), "p", "o");
786 }
787 let ce = CardinalityEstimator::from_graph_stats(&gs);
788 let join_card = ce.estimate_join(50, 50, 1);
789 assert_eq!(join_card, 25);
791 }
792
793 #[test]
794 fn test_cardinality_estimator_join_zero() {
795 let ce = CardinalityEstimator::new();
796 assert_eq!(ce.estimate_join(10, 10, 0), 0);
797 }
798
799 #[test]
800 fn test_cardinality_estimator_update_from() {
801 let ce_before = CardinalityEstimator::new();
802 assert_eq!(ce_before.estimate_pattern(false, None, false), 0);
803 let mut gs = GraphStatistics::new();
804 gs.add_triple("s", "p", "o");
805 let mut ce = CardinalityEstimator::new();
806 ce.update_from(&gs);
807 assert_eq!(ce.estimate_pattern(false, None, false), 1);
808 }
809
810 #[test]
813 fn test_sampled_statistics_empty() {
814 let ss = SampledStatistics::new(100);
815 assert_eq!(ss.total_seen(), 0);
816 assert_eq!(ss.sample_size(), 0);
817 }
818
819 #[test]
820 fn test_sampled_statistics_observe_fewer_than_capacity() {
821 let mut ss = SampledStatistics::new(100);
822 ss.observe("s", "p", "o");
823 ss.observe("s2", "p2", "o2");
824 assert_eq!(ss.total_seen(), 2);
825 assert_eq!(ss.sample_size(), 2);
826 }
827
828 #[test]
829 fn test_sampled_statistics_observe_more_than_capacity() {
830 let mut ss = SampledStatistics::new(10);
831 for i in 0..50 {
832 ss.observe(&format!("s{i}"), "p", &format!("o{i}"));
833 }
834 assert_eq!(ss.total_seen(), 50);
835 assert_eq!(ss.sample_size(), 10);
836 }
837
838 #[test]
839 fn test_sampled_statistics_estimated_total() {
840 let mut ss = SampledStatistics::new(100);
841 for i in 0..200 {
842 ss.observe(&format!("s{i}"), "p", "o");
843 }
844 assert_eq!(ss.estimated_total(), 200);
845 }
846
847 #[test]
848 fn test_sampled_statistics_cardinality_from_sample() {
849 let mut ss = SampledStatistics::new(1000);
850 for i in 0..100 {
851 ss.observe(&format!("s{i}"), "p", "o");
852 }
853 let card = ss.estimate_cardinality(None, Some("p"), None);
854 assert!(card > 0);
855 }
856
857 #[test]
858 fn test_sampled_statistics_estimated_distinct_predicates() {
859 let mut ss = SampledStatistics::new(50);
860 for i in 0..5 {
861 ss.observe("s", &format!("p{i}"), "o");
862 }
863 let est = ss.estimated_distinct_predicates();
864 assert!(est >= 5);
865 }
866
867 #[test]
868 fn test_sampled_statistics_graph_stats_available() {
869 let mut ss = SampledStatistics::new(100);
870 ss.observe("alice", "knows", "bob");
871 let gs = ss.graph_stats();
872 assert_eq!(gs.total_triples(), 1);
873 }
874}