1use std::collections::HashSet;
22use std::sync::Arc;
23
24use arcstr::ArcStr;
25use grafeo_common::types::{EdgeId, EpochId, NodeId, PropertyKey, TransactionId, Value};
26use grafeo_common::utils::hash::FxHashMap;
27
28use super::Direction;
29use super::lpg::{CompareOp, Edge, Node};
30use super::traits::{GraphStore, GraphStoreSearch};
31use crate::statistics::Statistics;
32
33#[derive(Debug, Clone, Default)]
35pub struct ProjectionSpec {
36 node_labels: HashSet<String>,
38 edge_types: HashSet<String>,
40}
41
42impl ProjectionSpec {
43 #[must_use]
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 #[must_use]
51 pub fn with_node_labels(mut self, labels: impl IntoIterator<Item = impl Into<String>>) -> Self {
52 self.node_labels = labels.into_iter().map(Into::into).collect();
53 self
54 }
55
56 #[must_use]
58 pub fn with_edge_types(mut self, types: impl IntoIterator<Item = impl Into<String>>) -> Self {
59 self.edge_types = types.into_iter().map(Into::into).collect();
60 self
61 }
62
63 fn filters_labels(&self) -> bool {
65 !self.node_labels.is_empty()
66 }
67
68 fn filters_edge_types(&self) -> bool {
70 !self.edge_types.is_empty()
71 }
72}
73
74pub struct GraphProjection {
80 inner: Arc<dyn GraphStoreSearch>,
81 spec: ProjectionSpec,
82}
83
84impl GraphProjection {
85 pub fn new(inner: Arc<dyn GraphStoreSearch>, spec: ProjectionSpec) -> Self {
87 Self { inner, spec }
88 }
89
90 fn node_matches(&self, node: &Node) -> bool {
92 if !self.spec.filters_labels() {
93 return true;
94 }
95 node.labels
96 .iter()
97 .any(|l| self.spec.node_labels.contains(l.as_str()))
98 }
99
100 fn node_id_matches(&self, id: NodeId) -> bool {
102 if !self.spec.filters_labels() {
103 return true;
104 }
105 self.inner
106 .get_node(id)
107 .is_some_and(|n| self.node_matches(&n))
108 }
109
110 fn edge_type_matches(&self, edge_type: &str) -> bool {
112 if !self.spec.filters_edge_types() {
113 return true;
114 }
115 self.spec.edge_types.contains(edge_type)
116 }
117
118 fn edge_matches(&self, edge: &Edge) -> bool {
120 if !self.edge_type_matches(&edge.edge_type) {
121 return false;
122 }
123 self.node_id_matches(edge.src) && self.node_id_matches(edge.dst)
124 }
125}
126
127impl GraphStore for GraphProjection {
128 fn get_node(&self, id: NodeId) -> Option<Node> {
131 self.inner.get_node(id).filter(|n| self.node_matches(n))
132 }
133
134 fn get_edge(&self, id: EdgeId) -> Option<Edge> {
135 self.inner.get_edge(id).filter(|e| self.edge_matches(e))
136 }
137
138 fn get_node_versioned(
139 &self,
140 id: NodeId,
141 epoch: EpochId,
142 transaction_id: TransactionId,
143 ) -> Option<Node> {
144 self.inner
145 .get_node_versioned(id, epoch, transaction_id)
146 .filter(|n| self.node_matches(n))
147 }
148
149 fn get_edge_versioned(
156 &self,
157 id: EdgeId,
158 epoch: EpochId,
159 transaction_id: TransactionId,
160 ) -> Option<Edge> {
161 self.inner
162 .get_edge_versioned(id, epoch, transaction_id)
163 .filter(|e| self.edge_matches(e))
164 }
165
166 fn get_node_at_epoch(&self, id: NodeId, epoch: EpochId) -> Option<Node> {
167 self.inner
168 .get_node_at_epoch(id, epoch)
169 .filter(|n| self.node_matches(n))
170 }
171
172 fn get_edge_at_epoch(&self, id: EdgeId, epoch: EpochId) -> Option<Edge> {
173 self.inner
174 .get_edge_at_epoch(id, epoch)
175 .filter(|e| self.edge_matches(e))
176 }
177
178 fn get_node_property(&self, id: NodeId, key: &PropertyKey) -> Option<Value> {
181 if !self.node_id_matches(id) {
182 return None;
183 }
184 self.inner.get_node_property(id, key)
185 }
186
187 fn get_edge_property(&self, id: EdgeId, key: &PropertyKey) -> Option<Value> {
188 self.inner
189 .get_edge(id)
190 .filter(|e| self.edge_matches(e))
191 .and_then(|_| self.inner.get_edge_property(id, key))
192 }
193
194 fn get_node_property_batch(&self, ids: &[NodeId], key: &PropertyKey) -> Vec<Option<Value>> {
195 let filtered: Vec<_> = ids
196 .iter()
197 .map(|&id| {
198 if self.node_id_matches(id) {
199 self.inner.get_node_property(id, key)
200 } else {
201 None
202 }
203 })
204 .collect();
205 filtered
206 }
207
208 fn get_nodes_properties_batch(&self, ids: &[NodeId]) -> Vec<FxHashMap<PropertyKey, Value>> {
209 ids.iter()
210 .map(|&id| {
211 if self.node_id_matches(id) {
212 self.inner
213 .get_nodes_properties_batch(std::slice::from_ref(&id))
214 .into_iter()
215 .next()
216 .unwrap_or_default()
217 } else {
218 FxHashMap::default()
219 }
220 })
221 .collect()
222 }
223
224 fn get_nodes_properties_selective_batch(
225 &self,
226 ids: &[NodeId],
227 keys: &[PropertyKey],
228 ) -> Vec<FxHashMap<PropertyKey, Value>> {
229 ids.iter()
230 .map(|&id| {
231 if self.node_id_matches(id) {
232 self.inner
233 .get_nodes_properties_selective_batch(std::slice::from_ref(&id), keys)
234 .into_iter()
235 .next()
236 .unwrap_or_default()
237 } else {
238 FxHashMap::default()
239 }
240 })
241 .collect()
242 }
243
244 fn get_edges_properties_selective_batch(
245 &self,
246 ids: &[EdgeId],
247 keys: &[PropertyKey],
248 ) -> Vec<FxHashMap<PropertyKey, Value>> {
249 ids.iter()
250 .map(|&id| {
251 if self.get_edge(id).is_some() {
252 self.inner
253 .get_edges_properties_selective_batch(std::slice::from_ref(&id), keys)
254 .into_iter()
255 .next()
256 .unwrap_or_default()
257 } else {
258 FxHashMap::default()
259 }
260 })
261 .collect()
262 }
263
264 fn neighbors(&self, node: NodeId, direction: Direction) -> Vec<NodeId> {
267 if !self.node_id_matches(node) {
268 return Vec::new();
269 }
270 self.edges_from(node, direction)
274 .into_iter()
275 .map(|(target, _)| target)
276 .collect()
277 }
278
279 fn edges_from(&self, node: NodeId, direction: Direction) -> Vec<(NodeId, EdgeId)> {
280 if !self.node_id_matches(node) {
281 return Vec::new();
282 }
283 self.inner
284 .edges_from(node, direction)
285 .into_iter()
286 .filter(|&(target, edge_id)| {
287 self.node_id_matches(target)
288 && self
289 .inner
290 .edge_type(edge_id)
291 .is_some_and(|t| self.edge_type_matches(&t))
292 })
293 .collect()
294 }
295
296 fn out_degree(&self, node: NodeId) -> usize {
297 self.edges_from(node, Direction::Outgoing).len()
298 }
299
300 fn in_degree(&self, node: NodeId) -> usize {
301 self.edges_from(node, Direction::Incoming).len()
302 }
303
304 fn has_backward_adjacency(&self) -> bool {
305 self.inner.has_backward_adjacency()
306 }
307
308 fn node_ids(&self) -> Vec<NodeId> {
311 if !self.spec.filters_labels() {
312 return self.inner.node_ids();
313 }
314 self.inner
315 .node_ids()
316 .into_iter()
317 .filter(|&id| self.node_id_matches(id))
318 .collect()
319 }
320
321 fn all_node_ids(&self) -> Vec<NodeId> {
322 if !self.spec.filters_labels() {
323 return self.inner.all_node_ids();
324 }
325 self.inner
326 .all_node_ids()
327 .into_iter()
328 .filter(|&id| self.node_id_matches(id))
329 .collect()
330 }
331
332 fn nodes_by_label(&self, label: &str) -> Vec<NodeId> {
333 if self.spec.filters_labels() && !self.spec.node_labels.contains(label) {
334 return Vec::new();
335 }
336 self.inner.nodes_by_label(label)
337 }
338
339 fn nodes_by_label_count(&self, label: &str) -> usize {
340 if self.spec.filters_labels() && !self.spec.node_labels.contains(label) {
341 return 0;
342 }
343 self.inner.nodes_by_label_count(label)
344 }
345
346 fn node_count(&self) -> usize {
347 self.node_ids().len()
348 }
349
350 fn edge_count(&self) -> usize {
351 if !self.spec.filters_edge_types() && !self.spec.filters_labels() {
353 return self.inner.edge_count();
354 }
355 self.node_ids().iter().map(|&id| self.out_degree(id)).sum()
357 }
358
359 fn edge_type(&self, id: EdgeId) -> Option<ArcStr> {
362 let et = self.inner.edge_type(id)?;
364 if !self.edge_type_matches(&et) {
365 return None;
366 }
367 if self.spec.filters_labels() {
369 let edge = self.inner.get_edge(id)?;
370 if !self.node_id_matches(edge.src) || !self.node_id_matches(edge.dst) {
371 return None;
372 }
373 }
374 Some(et)
375 }
376
377 fn edge_type_versioned(
382 &self,
383 id: EdgeId,
384 epoch: EpochId,
385 transaction_id: TransactionId,
386 ) -> Option<ArcStr> {
387 let et = self.inner.edge_type_versioned(id, epoch, transaction_id)?;
388 if !self.edge_type_matches(&et) {
389 return None;
390 }
391 if self.spec.filters_labels() {
392 let edge = self.inner.get_edge_versioned(id, epoch, transaction_id)?;
393 if !self.node_id_matches(edge.src) || !self.node_id_matches(edge.dst) {
394 return None;
395 }
396 }
397 Some(et)
398 }
399
400 fn has_property_index(&self, property: &str) -> bool {
403 self.inner.has_property_index(property)
404 }
405
406 fn find_nodes_by_property(&self, property: &str, value: &Value) -> Vec<NodeId> {
409 self.inner
410 .find_nodes_by_property(property, value)
411 .into_iter()
412 .filter(|&id| self.node_id_matches(id))
413 .collect()
414 }
415
416 fn find_nodes_by_properties(&self, conditions: &[(&str, Value)]) -> Vec<NodeId> {
417 self.inner
418 .find_nodes_by_properties(conditions)
419 .into_iter()
420 .filter(|&id| self.node_id_matches(id))
421 .collect()
422 }
423
424 fn find_nodes_in_range(
425 &self,
426 property: &str,
427 min: Option<&Value>,
428 max: Option<&Value>,
429 min_inclusive: bool,
430 max_inclusive: bool,
431 ) -> Vec<NodeId> {
432 self.inner
433 .find_nodes_in_range(property, min, max, min_inclusive, max_inclusive)
434 .into_iter()
435 .filter(|&id| self.node_id_matches(id))
436 .collect()
437 }
438
439 fn node_property_might_match(
442 &self,
443 property: &PropertyKey,
444 op: CompareOp,
445 value: &Value,
446 ) -> bool {
447 self.inner.node_property_might_match(property, op, value)
448 }
449
450 fn edge_property_might_match(
451 &self,
452 property: &PropertyKey,
453 op: CompareOp,
454 value: &Value,
455 ) -> bool {
456 self.inner.edge_property_might_match(property, op, value)
457 }
458
459 fn statistics(&self) -> Arc<Statistics> {
462 self.inner.statistics()
463 }
464
465 fn estimate_label_cardinality(&self, label: &str) -> f64 {
466 if self.spec.filters_labels() && !self.spec.node_labels.contains(label) {
467 return 0.0;
468 }
469 self.inner.estimate_label_cardinality(label)
470 }
471
472 fn estimate_avg_degree(&self, edge_type: &str, outgoing: bool) -> f64 {
473 if self.spec.filters_edge_types() && !self.spec.edge_types.contains(edge_type) {
474 return 0.0;
475 }
476 self.inner.estimate_avg_degree(edge_type, outgoing)
477 }
478
479 fn current_epoch(&self) -> EpochId {
482 self.inner.current_epoch()
483 }
484
485 fn all_labels(&self) -> Vec<String> {
488 if self.spec.filters_labels() {
489 self.spec.node_labels.iter().cloned().collect()
490 } else {
491 self.inner.all_labels()
492 }
493 }
494
495 fn all_edge_types(&self) -> Vec<String> {
496 if self.spec.filters_edge_types() {
497 self.spec.edge_types.iter().cloned().collect()
498 } else {
499 self.inner.all_edge_types()
500 }
501 }
502
503 fn all_property_keys(&self) -> Vec<String> {
504 self.inner.all_property_keys()
505 }
506}
507
508impl GraphStoreSearch for GraphProjection {}
509
510#[cfg(test)]
511#[cfg(feature = "lpg")]
512mod tests {
513 use super::*;
514 use crate::graph::lpg::LpgStore;
515
516 fn setup_social_graph() -> Arc<LpgStore> {
517 let store = Arc::new(LpgStore::new().unwrap());
518 let alix = store.create_node(&["Person"]);
519 let gus = store.create_node(&["Person"]);
520 let amsterdam = store.create_node(&["City"]);
521 let grafeo = store.create_node(&["Software"]);
522
523 store.set_node_property(alix, "name", Value::from("Alix"));
524 store.set_node_property(gus, "name", Value::from("Gus"));
525 store.set_node_property(amsterdam, "name", Value::from("Amsterdam"));
526 store.set_node_property(grafeo, "name", Value::from("Grafeo"));
527
528 store.create_edge(alix, gus, "KNOWS");
529 store.create_edge(alix, amsterdam, "LIVES_IN");
530 store.create_edge(gus, amsterdam, "LIVES_IN");
531 store.create_edge(alix, grafeo, "CONTRIBUTES_TO");
532
533 store
534 }
535
536 #[test]
537 fn unfiltered_projection_sees_everything() {
538 let store = setup_social_graph();
539 let proj = GraphProjection::new(store.clone(), ProjectionSpec::new());
540 assert_eq!(proj.node_count(), store.node_count());
541 assert_eq!(proj.edge_count(), store.edge_count());
542 }
543
544 #[test]
545 fn filter_by_label() {
546 let store = setup_social_graph();
547 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
548 let proj = GraphProjection::new(store, spec);
549
550 assert_eq!(proj.node_count(), 2);
551 assert_eq!(proj.nodes_by_label("Person").len(), 2);
552 assert!(proj.nodes_by_label("City").is_empty());
553 assert!(proj.nodes_by_label("Software").is_empty());
554 }
555
556 #[test]
557 fn filter_by_edge_type() {
558 let store = setup_social_graph();
559 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
560 let proj = GraphProjection::new(store, spec);
561
562 assert_eq!(proj.node_count(), 4);
564 assert_eq!(proj.edge_count(), 1);
565 }
566
567 #[test]
568 fn combined_label_and_edge_filter() {
569 let store = setup_social_graph();
570 let spec = ProjectionSpec::new()
571 .with_node_labels(["Person", "City"])
572 .with_edge_types(["LIVES_IN"]);
573 let proj = GraphProjection::new(store, spec);
574
575 assert_eq!(proj.node_count(), 3); assert_eq!(proj.edge_count(), 2); }
578
579 #[test]
580 fn edge_excluded_when_endpoint_excluded() {
581 let store = setup_social_graph();
582 let spec = ProjectionSpec::new()
585 .with_node_labels(["Person"])
586 .with_edge_types(["LIVES_IN"]);
587 let proj = GraphProjection::new(store, spec);
588
589 assert_eq!(proj.node_count(), 2);
590 assert_eq!(proj.edge_count(), 0);
592 }
593
594 #[test]
595 fn get_node_filtered() {
596 let store = setup_social_graph();
597 let all_ids = store.node_ids();
598 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
599 let proj = GraphProjection::new(store.clone(), spec);
600
601 assert!(proj.get_node(all_ids[0]).is_some()); assert!(proj.get_node(all_ids[1]).is_some()); assert!(proj.get_node(all_ids[2]).is_none()); assert!(proj.get_node(all_ids[3]).is_none()); }
608
609 #[test]
610 fn neighbors_filtered() {
611 let store = setup_social_graph();
612 let alix_id = store.node_ids()[0];
613
614 let all_neighbors: Vec<_> = store.neighbors(alix_id, Direction::Outgoing).collect();
616 assert_eq!(all_neighbors.len(), 3);
617
618 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
620 let proj = GraphProjection::new(store, spec);
621 let neighbors = proj.neighbors(alix_id, Direction::Outgoing);
622 assert_eq!(neighbors.len(), 1);
623 }
624
625 #[test]
626 fn neighbors_filtered_by_edge_type() {
627 let store = setup_social_graph();
628 let alix_id = store.node_ids()[0];
629
630 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
633 let proj = GraphProjection::new(store, spec);
634 let neighbors = proj.neighbors(alix_id, Direction::Outgoing);
635 assert_eq!(neighbors.len(), 1);
636 }
637
638 #[test]
639 fn property_access_respects_filter() {
640 let store = setup_social_graph();
641 let city_id = store.node_ids()[2]; let spec = ProjectionSpec::new().with_node_labels(["Person"]);
643 let proj = GraphProjection::new(store, spec);
644
645 assert!(
647 proj.get_node_property(city_id, &PropertyKey::from("name"))
648 .is_none()
649 );
650 }
651
652 #[test]
653 fn cardinality_estimation_respects_filter() {
654 let store = setup_social_graph();
655 let spec = ProjectionSpec::new()
656 .with_node_labels(["Person"])
657 .with_edge_types(["KNOWS"]);
658 let proj = GraphProjection::new(store, spec);
659
660 assert!(proj.estimate_label_cardinality("City") == 0.0);
661 assert!(proj.estimate_avg_degree("LIVES_IN", true) == 0.0);
662 }
663
664 #[test]
665 fn schema_introspection_reflects_filter() {
666 let store = setup_social_graph();
667 let spec = ProjectionSpec::new()
668 .with_node_labels(["Person"])
669 .with_edge_types(["KNOWS"]);
670 let proj = GraphProjection::new(store, spec);
671
672 let labels = proj.all_labels();
673 assert_eq!(labels.len(), 1);
674 assert!(labels.contains(&"Person".to_string()));
675
676 let edge_types = proj.all_edge_types();
677 assert_eq!(edge_types.len(), 1);
678 assert!(edge_types.contains(&"KNOWS".to_string()));
679 }
680
681 fn setup_social_graph_with_ids() -> (Arc<LpgStore>, Vec<NodeId>, Vec<EdgeId>) {
683 let store = Arc::new(LpgStore::new().unwrap());
684 let alix = store.create_node(&["Person"]);
685 let gus = store.create_node(&["Person"]);
686 let amsterdam = store.create_node(&["City"]);
687 let grafeo = store.create_node(&["Software"]);
688
689 store.set_node_property(alix, "name", Value::from("Alix"));
690 store.set_node_property(gus, "name", Value::from("Gus"));
691 store.set_node_property(amsterdam, "name", Value::from("Amsterdam"));
692 store.set_node_property(grafeo, "name", Value::from("Grafeo"));
693 store.set_node_property(alix, "age", Value::from(30));
694 store.set_node_property(gus, "age", Value::from(25));
695
696 let e_knows = store.create_edge(alix, gus, "KNOWS");
697 let e_alix_lives = store.create_edge(alix, amsterdam, "LIVES_IN");
698 let e_gus_lives = store.create_edge(gus, amsterdam, "LIVES_IN");
699 let e_contrib = store.create_edge(alix, grafeo, "CONTRIBUTES_TO");
700
701 store.set_edge_property(e_knows, "since", Value::from(2020));
702 store.set_edge_property(e_alix_lives, "since", Value::from(2018));
703
704 let nodes = vec![alix, gus, amsterdam, grafeo];
705 let edges = vec![e_knows, e_alix_lives, e_gus_lives, e_contrib];
706 (store, nodes, edges)
707 }
708
709 #[test]
712 fn get_edge_passes_type_filter() {
713 let (store, _, edges) = setup_social_graph_with_ids();
714 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
715 let proj = GraphProjection::new(store, spec);
716
717 assert!(proj.get_edge(edges[0]).is_some());
719 assert!(proj.get_edge(edges[1]).is_none());
721 assert!(proj.get_edge(edges[3]).is_none());
723 }
724
725 #[test]
726 fn get_edge_excluded_by_endpoint_label_filter() {
727 let (store, _, edges) = setup_social_graph_with_ids();
728 let spec = ProjectionSpec::new()
730 .with_node_labels(["Person"])
731 .with_edge_types(["LIVES_IN"]);
732 let proj = GraphProjection::new(store, spec);
733
734 assert!(proj.get_edge(edges[1]).is_none()); assert!(proj.get_edge(edges[2]).is_none()); }
737
738 #[test]
741 fn get_node_versioned_respects_filter() {
742 let (store, nodes, _) = setup_social_graph_with_ids();
743 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
744 let proj = GraphProjection::new(store, spec);
745
746 let epoch = EpochId(0);
747 let txn = TransactionId(0);
748
749 assert!(proj.get_node_versioned(nodes[0], epoch, txn).is_some());
751 assert!(proj.get_node_versioned(nodes[2], epoch, txn).is_none());
753 }
754
755 #[test]
756 fn get_edge_versioned_respects_filter() {
757 let (store, _, edges) = setup_social_graph_with_ids();
758 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
759 let proj = GraphProjection::new(store, spec);
760
761 let epoch = EpochId(0);
762 let txn = TransactionId(0);
763
764 assert!(proj.get_edge_versioned(edges[0], epoch, txn).is_some());
766 assert!(proj.get_edge_versioned(edges[1], epoch, txn).is_none());
768 }
769
770 #[test]
773 fn get_node_at_epoch_respects_filter() {
774 let (store, nodes, _) = setup_social_graph_with_ids();
775 let spec = ProjectionSpec::new().with_node_labels(["City"]);
776 let proj = GraphProjection::new(store, spec);
777
778 let epoch = EpochId(0);
779
780 assert!(proj.get_node_at_epoch(nodes[2], epoch).is_some());
782 assert!(proj.get_node_at_epoch(nodes[0], epoch).is_none());
784 }
785
786 #[test]
787 fn get_edge_at_epoch_respects_filter() {
788 let (store, _, edges) = setup_social_graph_with_ids();
789 let spec = ProjectionSpec::new().with_edge_types(["LIVES_IN"]);
790 let proj = GraphProjection::new(store, spec);
791
792 let epoch = EpochId(0);
793
794 assert!(proj.get_edge_at_epoch(edges[1], epoch).is_some());
796 assert!(proj.get_edge_at_epoch(edges[0], epoch).is_none());
798 }
799
800 #[test]
803 fn get_edge_property_in_projection() {
804 let (store, _, edges) = setup_social_graph_with_ids();
805 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
806 let proj = GraphProjection::new(store, spec);
807
808 let key = PropertyKey::from("since");
809 assert_eq!(
811 proj.get_edge_property(edges[0], &key),
812 Some(Value::from(2020))
813 );
814 }
815
816 #[test]
817 fn get_edge_property_outside_projection() {
818 let (store, _, edges) = setup_social_graph_with_ids();
819 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
820 let proj = GraphProjection::new(store, spec);
821
822 let key = PropertyKey::from("since");
823 assert!(proj.get_edge_property(edges[1], &key).is_none());
825 }
826
827 #[test]
830 fn get_node_property_batch_mixed() {
831 let (store, nodes, _) = setup_social_graph_with_ids();
832 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
833 let proj = GraphProjection::new(store, spec);
834
835 let key = PropertyKey::from("name");
836 let ids = vec![nodes[0], nodes[2], nodes[1]];
838 let results = proj.get_node_property_batch(&ids, &key);
839
840 assert_eq!(results.len(), 3);
841 assert_eq!(results[0], Some(Value::from("Alix"))); assert_eq!(results[1], None); assert_eq!(results[2], Some(Value::from("Gus"))); }
845
846 #[test]
849 fn get_nodes_properties_batch_filters() {
850 let (store, nodes, _) = setup_social_graph_with_ids();
851 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
852 let proj = GraphProjection::new(store, spec);
853
854 let ids = vec![nodes[0], nodes[2]]; let results = proj.get_nodes_properties_batch(&ids);
856
857 assert_eq!(results.len(), 2);
858 assert!(results[0].contains_key(&PropertyKey::from("name")));
860 assert!(results[1].is_empty());
862 }
863
864 #[test]
865 fn get_nodes_properties_selective_batch_filters() {
866 let (store, nodes, _) = setup_social_graph_with_ids();
867 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
868 let proj = GraphProjection::new(store, spec);
869
870 let ids = vec![nodes[0], nodes[2]]; let keys = vec![PropertyKey::from("name")];
872 let results = proj.get_nodes_properties_selective_batch(&ids, &keys);
873
874 assert_eq!(results.len(), 2);
875 assert_eq!(
876 results[0].get(&PropertyKey::from("name")),
877 Some(&Value::from("Alix"))
878 );
879 assert!(results[1].is_empty());
880 }
881
882 #[test]
885 fn get_edges_properties_selective_batch_filters() {
886 let (store, _, edges) = setup_social_graph_with_ids();
887 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
888 let proj = GraphProjection::new(store, spec);
889
890 let ids = vec![edges[0], edges[1]]; let keys = vec![PropertyKey::from("since")];
892 let results = proj.get_edges_properties_selective_batch(&ids, &keys);
893
894 assert_eq!(results.len(), 2);
895 assert_eq!(
897 results[0].get(&PropertyKey::from("since")),
898 Some(&Value::from(2020))
899 );
900 assert!(results[1].is_empty());
902 }
903
904 #[test]
907 fn edges_from_with_edge_type_filter() {
908 let (store, nodes, _) = setup_social_graph_with_ids();
909 let spec = ProjectionSpec::new().with_edge_types(["LIVES_IN"]);
910 let proj = GraphProjection::new(store, spec);
911
912 let alix_edges = proj.edges_from(nodes[0], Direction::Outgoing);
915 assert_eq!(alix_edges.len(), 1);
916 assert_eq!(alix_edges[0].0, nodes[2]); }
918
919 #[test]
920 fn edges_from_filtered_node_returns_empty() {
921 let (store, nodes, _) = setup_social_graph_with_ids();
922 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
923 let proj = GraphProjection::new(store, spec);
924
925 let amsterdam_edges = proj.edges_from(nodes[2], Direction::Outgoing);
927 assert!(amsterdam_edges.is_empty());
928 }
929
930 #[test]
933 fn out_degree_with_filter() {
934 let (store, nodes, _) = setup_social_graph_with_ids();
935 let spec = ProjectionSpec::new()
936 .with_node_labels(["Person", "City"])
937 .with_edge_types(["LIVES_IN"]);
938 let proj = GraphProjection::new(store, spec);
939
940 assert_eq!(proj.out_degree(nodes[0]), 1);
942 assert_eq!(proj.out_degree(nodes[1]), 1);
944 assert_eq!(proj.out_degree(nodes[2]), 0);
946 }
947
948 #[test]
949 fn in_degree_with_filter() {
950 let (store, nodes, _) = setup_social_graph_with_ids();
951 let spec = ProjectionSpec::new()
952 .with_node_labels(["Person", "City"])
953 .with_edge_types(["LIVES_IN"]);
954 let proj = GraphProjection::new(store, spec);
955
956 assert_eq!(proj.in_degree(nodes[2]), 2);
958 assert_eq!(proj.in_degree(nodes[0]), 0);
960 }
961
962 #[test]
965 fn all_node_ids_with_label_filter() {
966 let (store, nodes, _) = setup_social_graph_with_ids();
967 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
968 let proj = GraphProjection::new(store, spec);
969
970 let ids = proj.all_node_ids();
971 assert_eq!(ids.len(), 2);
972 assert!(ids.contains(&nodes[0])); assert!(ids.contains(&nodes[1])); assert!(!ids.contains(&nodes[2])); }
976
977 #[test]
978 fn all_node_ids_unfiltered() {
979 let (store, _, _) = setup_social_graph_with_ids();
980 let spec = ProjectionSpec::new();
981 let proj = GraphProjection::new(store.clone(), spec);
982
983 assert_eq!(proj.all_node_ids().len(), store.all_node_ids().len());
984 }
985
986 #[test]
989 fn node_count_with_city_filter() {
990 let (store, _, _) = setup_social_graph_with_ids();
991 let spec = ProjectionSpec::new().with_node_labels(["City"]);
992 let proj = GraphProjection::new(store, spec);
993
994 assert_eq!(proj.node_count(), 1);
995 }
996
997 #[test]
998 fn edge_count_with_combined_filter() {
999 let (store, _, _) = setup_social_graph_with_ids();
1000 let spec = ProjectionSpec::new()
1001 .with_node_labels(["Person"])
1002 .with_edge_types(["KNOWS"]);
1003 let proj = GraphProjection::new(store, spec);
1004
1005 assert_eq!(proj.edge_count(), 1);
1007 }
1008
1009 #[test]
1010 fn edge_count_unfiltered_delegates() {
1011 let (store, _, _) = setup_social_graph_with_ids();
1012 let spec = ProjectionSpec::new();
1013 let proj = GraphProjection::new(store.clone(), spec);
1014
1015 assert_eq!(proj.edge_count(), store.edge_count());
1016 }
1017
1018 #[test]
1021 fn find_nodes_by_property_with_label_filter() {
1022 let (store, nodes, _) = setup_social_graph_with_ids();
1023 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1024 let proj = GraphProjection::new(store, spec);
1025
1026 let found = proj.find_nodes_by_property("name", &Value::from("Alix"));
1028 assert_eq!(found.len(), 1);
1029 assert_eq!(found[0], nodes[0]);
1030
1031 let found = proj.find_nodes_by_property("name", &Value::from("Amsterdam"));
1033 assert!(found.is_empty());
1034 }
1035
1036 #[test]
1039 fn find_nodes_by_properties_with_label_filter() {
1040 let (store, nodes, _) = setup_social_graph_with_ids();
1041 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1042 let proj = GraphProjection::new(store, spec);
1043
1044 let conditions = vec![("name", Value::from("Gus"))];
1045 let found = proj.find_nodes_by_properties(&conditions);
1046 assert_eq!(found.len(), 1);
1047 assert_eq!(found[0], nodes[1]);
1048
1049 let conditions = vec![("name", Value::from("Amsterdam"))];
1051 let found = proj.find_nodes_by_properties(&conditions);
1052 assert!(found.is_empty());
1053 }
1054
1055 #[test]
1058 fn find_nodes_in_range_with_label_filter() {
1059 let (store, nodes, _) = setup_social_graph_with_ids();
1060 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1061 let proj = GraphProjection::new(store, spec);
1062
1063 let min = Value::from(20);
1065 let max = Value::from(30);
1066 let found = proj.find_nodes_in_range("age", Some(&min), Some(&max), true, true);
1067 assert_eq!(found.len(), 2);
1068 assert!(found.contains(&nodes[0])); assert!(found.contains(&nodes[1])); }
1071
1072 #[test]
1073 fn find_nodes_in_range_excludes_filtered_labels() {
1074 let (store, _, _) = setup_social_graph_with_ids();
1075 let spec = ProjectionSpec::new().with_node_labels(["City"]);
1076 let proj = GraphProjection::new(store, spec);
1077
1078 let min = Value::from(20);
1080 let max = Value::from(30);
1081 let found = proj.find_nodes_in_range("age", Some(&min), Some(&max), true, true);
1082 assert!(found.is_empty());
1083 }
1084
1085 #[test]
1088 fn node_property_might_match_delegates() {
1089 let (store, _, _) = setup_social_graph_with_ids();
1090 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1091 let proj = GraphProjection::new(store.clone(), spec);
1092
1093 let key = PropertyKey::from("name");
1094 let val = Value::from("Alix");
1095 let inner_result = store.node_property_might_match(&key, CompareOp::Eq, &val);
1097 assert_eq!(
1098 proj.node_property_might_match(&key, CompareOp::Eq, &val),
1099 inner_result
1100 );
1101 }
1102
1103 #[test]
1104 fn edge_property_might_match_delegates() {
1105 let (store, _, _) = setup_social_graph_with_ids();
1106 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
1107 let proj = GraphProjection::new(store.clone(), spec);
1108
1109 let key = PropertyKey::from("since");
1110 let val = Value::from(2020);
1111 let inner_result = store.edge_property_might_match(&key, CompareOp::Eq, &val);
1112 assert_eq!(
1113 proj.edge_property_might_match(&key, CompareOp::Eq, &val),
1114 inner_result
1115 );
1116 }
1117
1118 #[test]
1121 fn current_epoch_delegates() {
1122 let (store, _, _) = setup_social_graph_with_ids();
1123 let spec = ProjectionSpec::new();
1124 let proj = GraphProjection::new(store.clone(), spec);
1125
1126 assert_eq!(proj.current_epoch(), store.current_epoch());
1127 }
1128
1129 #[test]
1132 fn all_property_keys_delegates() {
1133 let (store, _, _) = setup_social_graph_with_ids();
1134 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1135 let proj = GraphProjection::new(store.clone(), spec);
1136
1137 let proj_keys = proj.all_property_keys();
1138 let store_keys = store.all_property_keys();
1139 assert_eq!(proj_keys.len(), store_keys.len());
1141 }
1142
1143 #[test]
1146 fn statistics_returns_value() {
1147 let (store, _, _) = setup_social_graph_with_ids();
1148 let spec = ProjectionSpec::new();
1149 let proj = GraphProjection::new(store, spec);
1150
1151 let stats = proj.statistics();
1152 let _ = stats;
1154 }
1155
1156 #[test]
1159 fn has_backward_adjacency_delegates() {
1160 let (store, _, _) = setup_social_graph_with_ids();
1161 let spec = ProjectionSpec::new();
1162 let proj = GraphProjection::new(store.clone(), spec);
1163
1164 assert_eq!(
1165 proj.has_backward_adjacency(),
1166 store.has_backward_adjacency()
1167 );
1168 }
1169
1170 #[test]
1173 fn has_property_index_delegates() {
1174 let (store, _, _) = setup_social_graph_with_ids();
1175 let spec = ProjectionSpec::new();
1176 let proj = GraphProjection::new(store.clone(), spec);
1177
1178 assert_eq!(
1180 proj.has_property_index("name"),
1181 store.has_property_index("name")
1182 );
1183 }
1184}