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 node_count(&self) -> usize {
340 self.node_ids().len()
341 }
342
343 fn edge_count(&self) -> usize {
344 if !self.spec.filters_edge_types() && !self.spec.filters_labels() {
346 return self.inner.edge_count();
347 }
348 self.node_ids().iter().map(|&id| self.out_degree(id)).sum()
350 }
351
352 fn edge_type(&self, id: EdgeId) -> Option<ArcStr> {
355 let et = self.inner.edge_type(id)?;
357 if !self.edge_type_matches(&et) {
358 return None;
359 }
360 if self.spec.filters_labels() {
362 let edge = self.inner.get_edge(id)?;
363 if !self.node_id_matches(edge.src) || !self.node_id_matches(edge.dst) {
364 return None;
365 }
366 }
367 Some(et)
368 }
369
370 fn edge_type_versioned(
375 &self,
376 id: EdgeId,
377 epoch: EpochId,
378 transaction_id: TransactionId,
379 ) -> Option<ArcStr> {
380 let et = self.inner.edge_type_versioned(id, epoch, transaction_id)?;
381 if !self.edge_type_matches(&et) {
382 return None;
383 }
384 if self.spec.filters_labels() {
385 let edge = self.inner.get_edge_versioned(id, epoch, transaction_id)?;
386 if !self.node_id_matches(edge.src) || !self.node_id_matches(edge.dst) {
387 return None;
388 }
389 }
390 Some(et)
391 }
392
393 fn has_property_index(&self, property: &str) -> bool {
396 self.inner.has_property_index(property)
397 }
398
399 fn find_nodes_by_property(&self, property: &str, value: &Value) -> Vec<NodeId> {
402 self.inner
403 .find_nodes_by_property(property, value)
404 .into_iter()
405 .filter(|&id| self.node_id_matches(id))
406 .collect()
407 }
408
409 fn find_nodes_by_properties(&self, conditions: &[(&str, Value)]) -> Vec<NodeId> {
410 self.inner
411 .find_nodes_by_properties(conditions)
412 .into_iter()
413 .filter(|&id| self.node_id_matches(id))
414 .collect()
415 }
416
417 fn find_nodes_in_range(
418 &self,
419 property: &str,
420 min: Option<&Value>,
421 max: Option<&Value>,
422 min_inclusive: bool,
423 max_inclusive: bool,
424 ) -> Vec<NodeId> {
425 self.inner
426 .find_nodes_in_range(property, min, max, min_inclusive, max_inclusive)
427 .into_iter()
428 .filter(|&id| self.node_id_matches(id))
429 .collect()
430 }
431
432 fn node_property_might_match(
435 &self,
436 property: &PropertyKey,
437 op: CompareOp,
438 value: &Value,
439 ) -> bool {
440 self.inner.node_property_might_match(property, op, value)
441 }
442
443 fn edge_property_might_match(
444 &self,
445 property: &PropertyKey,
446 op: CompareOp,
447 value: &Value,
448 ) -> bool {
449 self.inner.edge_property_might_match(property, op, value)
450 }
451
452 fn statistics(&self) -> Arc<Statistics> {
455 self.inner.statistics()
456 }
457
458 fn estimate_label_cardinality(&self, label: &str) -> f64 {
459 if self.spec.filters_labels() && !self.spec.node_labels.contains(label) {
460 return 0.0;
461 }
462 self.inner.estimate_label_cardinality(label)
463 }
464
465 fn estimate_avg_degree(&self, edge_type: &str, outgoing: bool) -> f64 {
466 if self.spec.filters_edge_types() && !self.spec.edge_types.contains(edge_type) {
467 return 0.0;
468 }
469 self.inner.estimate_avg_degree(edge_type, outgoing)
470 }
471
472 fn current_epoch(&self) -> EpochId {
475 self.inner.current_epoch()
476 }
477
478 fn all_labels(&self) -> Vec<String> {
481 if self.spec.filters_labels() {
482 self.spec.node_labels.iter().cloned().collect()
483 } else {
484 self.inner.all_labels()
485 }
486 }
487
488 fn all_edge_types(&self) -> Vec<String> {
489 if self.spec.filters_edge_types() {
490 self.spec.edge_types.iter().cloned().collect()
491 } else {
492 self.inner.all_edge_types()
493 }
494 }
495
496 fn all_property_keys(&self) -> Vec<String> {
497 self.inner.all_property_keys()
498 }
499}
500
501impl GraphStoreSearch for GraphProjection {}
502
503#[cfg(test)]
504#[cfg(feature = "lpg")]
505mod tests {
506 use super::*;
507 use crate::graph::lpg::LpgStore;
508
509 fn setup_social_graph() -> Arc<LpgStore> {
510 let store = Arc::new(LpgStore::new().unwrap());
511 let alix = store.create_node(&["Person"]);
512 let gus = store.create_node(&["Person"]);
513 let amsterdam = store.create_node(&["City"]);
514 let grafeo = store.create_node(&["Software"]);
515
516 store.set_node_property(alix, "name", Value::from("Alix"));
517 store.set_node_property(gus, "name", Value::from("Gus"));
518 store.set_node_property(amsterdam, "name", Value::from("Amsterdam"));
519 store.set_node_property(grafeo, "name", Value::from("Grafeo"));
520
521 store.create_edge(alix, gus, "KNOWS");
522 store.create_edge(alix, amsterdam, "LIVES_IN");
523 store.create_edge(gus, amsterdam, "LIVES_IN");
524 store.create_edge(alix, grafeo, "CONTRIBUTES_TO");
525
526 store
527 }
528
529 #[test]
530 fn unfiltered_projection_sees_everything() {
531 let store = setup_social_graph();
532 let proj = GraphProjection::new(store.clone(), ProjectionSpec::new());
533 assert_eq!(proj.node_count(), store.node_count());
534 assert_eq!(proj.edge_count(), store.edge_count());
535 }
536
537 #[test]
538 fn filter_by_label() {
539 let store = setup_social_graph();
540 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
541 let proj = GraphProjection::new(store, spec);
542
543 assert_eq!(proj.node_count(), 2);
544 assert_eq!(proj.nodes_by_label("Person").len(), 2);
545 assert!(proj.nodes_by_label("City").is_empty());
546 assert!(proj.nodes_by_label("Software").is_empty());
547 }
548
549 #[test]
550 fn filter_by_edge_type() {
551 let store = setup_social_graph();
552 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
553 let proj = GraphProjection::new(store, spec);
554
555 assert_eq!(proj.node_count(), 4);
557 assert_eq!(proj.edge_count(), 1);
558 }
559
560 #[test]
561 fn combined_label_and_edge_filter() {
562 let store = setup_social_graph();
563 let spec = ProjectionSpec::new()
564 .with_node_labels(["Person", "City"])
565 .with_edge_types(["LIVES_IN"]);
566 let proj = GraphProjection::new(store, spec);
567
568 assert_eq!(proj.node_count(), 3); assert_eq!(proj.edge_count(), 2); }
571
572 #[test]
573 fn edge_excluded_when_endpoint_excluded() {
574 let store = setup_social_graph();
575 let spec = ProjectionSpec::new()
578 .with_node_labels(["Person"])
579 .with_edge_types(["LIVES_IN"]);
580 let proj = GraphProjection::new(store, spec);
581
582 assert_eq!(proj.node_count(), 2);
583 assert_eq!(proj.edge_count(), 0);
585 }
586
587 #[test]
588 fn get_node_filtered() {
589 let store = setup_social_graph();
590 let all_ids = store.node_ids();
591 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
592 let proj = GraphProjection::new(store.clone(), spec);
593
594 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()); }
601
602 #[test]
603 fn neighbors_filtered() {
604 let store = setup_social_graph();
605 let alix_id = store.node_ids()[0];
606
607 let all_neighbors: Vec<_> = store.neighbors(alix_id, Direction::Outgoing).collect();
609 assert_eq!(all_neighbors.len(), 3);
610
611 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
613 let proj = GraphProjection::new(store, spec);
614 let neighbors = proj.neighbors(alix_id, Direction::Outgoing);
615 assert_eq!(neighbors.len(), 1);
616 }
617
618 #[test]
619 fn neighbors_filtered_by_edge_type() {
620 let store = setup_social_graph();
621 let alix_id = store.node_ids()[0];
622
623 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
626 let proj = GraphProjection::new(store, spec);
627 let neighbors = proj.neighbors(alix_id, Direction::Outgoing);
628 assert_eq!(neighbors.len(), 1);
629 }
630
631 #[test]
632 fn property_access_respects_filter() {
633 let store = setup_social_graph();
634 let city_id = store.node_ids()[2]; let spec = ProjectionSpec::new().with_node_labels(["Person"]);
636 let proj = GraphProjection::new(store, spec);
637
638 assert!(
640 proj.get_node_property(city_id, &PropertyKey::from("name"))
641 .is_none()
642 );
643 }
644
645 #[test]
646 fn cardinality_estimation_respects_filter() {
647 let store = setup_social_graph();
648 let spec = ProjectionSpec::new()
649 .with_node_labels(["Person"])
650 .with_edge_types(["KNOWS"]);
651 let proj = GraphProjection::new(store, spec);
652
653 assert!(proj.estimate_label_cardinality("City") == 0.0);
654 assert!(proj.estimate_avg_degree("LIVES_IN", true) == 0.0);
655 }
656
657 #[test]
658 fn schema_introspection_reflects_filter() {
659 let store = setup_social_graph();
660 let spec = ProjectionSpec::new()
661 .with_node_labels(["Person"])
662 .with_edge_types(["KNOWS"]);
663 let proj = GraphProjection::new(store, spec);
664
665 let labels = proj.all_labels();
666 assert_eq!(labels.len(), 1);
667 assert!(labels.contains(&"Person".to_string()));
668
669 let edge_types = proj.all_edge_types();
670 assert_eq!(edge_types.len(), 1);
671 assert!(edge_types.contains(&"KNOWS".to_string()));
672 }
673
674 fn setup_social_graph_with_ids() -> (Arc<LpgStore>, Vec<NodeId>, Vec<EdgeId>) {
676 let store = Arc::new(LpgStore::new().unwrap());
677 let alix = store.create_node(&["Person"]);
678 let gus = store.create_node(&["Person"]);
679 let amsterdam = store.create_node(&["City"]);
680 let grafeo = store.create_node(&["Software"]);
681
682 store.set_node_property(alix, "name", Value::from("Alix"));
683 store.set_node_property(gus, "name", Value::from("Gus"));
684 store.set_node_property(amsterdam, "name", Value::from("Amsterdam"));
685 store.set_node_property(grafeo, "name", Value::from("Grafeo"));
686 store.set_node_property(alix, "age", Value::from(30));
687 store.set_node_property(gus, "age", Value::from(25));
688
689 let e_knows = store.create_edge(alix, gus, "KNOWS");
690 let e_alix_lives = store.create_edge(alix, amsterdam, "LIVES_IN");
691 let e_gus_lives = store.create_edge(gus, amsterdam, "LIVES_IN");
692 let e_contrib = store.create_edge(alix, grafeo, "CONTRIBUTES_TO");
693
694 store.set_edge_property(e_knows, "since", Value::from(2020));
695 store.set_edge_property(e_alix_lives, "since", Value::from(2018));
696
697 let nodes = vec![alix, gus, amsterdam, grafeo];
698 let edges = vec![e_knows, e_alix_lives, e_gus_lives, e_contrib];
699 (store, nodes, edges)
700 }
701
702 #[test]
705 fn get_edge_passes_type_filter() {
706 let (store, _, edges) = setup_social_graph_with_ids();
707 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
708 let proj = GraphProjection::new(store, spec);
709
710 assert!(proj.get_edge(edges[0]).is_some());
712 assert!(proj.get_edge(edges[1]).is_none());
714 assert!(proj.get_edge(edges[3]).is_none());
716 }
717
718 #[test]
719 fn get_edge_excluded_by_endpoint_label_filter() {
720 let (store, _, edges) = setup_social_graph_with_ids();
721 let spec = ProjectionSpec::new()
723 .with_node_labels(["Person"])
724 .with_edge_types(["LIVES_IN"]);
725 let proj = GraphProjection::new(store, spec);
726
727 assert!(proj.get_edge(edges[1]).is_none()); assert!(proj.get_edge(edges[2]).is_none()); }
730
731 #[test]
734 fn get_node_versioned_respects_filter() {
735 let (store, nodes, _) = setup_social_graph_with_ids();
736 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
737 let proj = GraphProjection::new(store, spec);
738
739 let epoch = EpochId(0);
740 let txn = TransactionId(0);
741
742 assert!(proj.get_node_versioned(nodes[0], epoch, txn).is_some());
744 assert!(proj.get_node_versioned(nodes[2], epoch, txn).is_none());
746 }
747
748 #[test]
749 fn get_edge_versioned_respects_filter() {
750 let (store, _, edges) = setup_social_graph_with_ids();
751 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
752 let proj = GraphProjection::new(store, spec);
753
754 let epoch = EpochId(0);
755 let txn = TransactionId(0);
756
757 assert!(proj.get_edge_versioned(edges[0], epoch, txn).is_some());
759 assert!(proj.get_edge_versioned(edges[1], epoch, txn).is_none());
761 }
762
763 #[test]
766 fn get_node_at_epoch_respects_filter() {
767 let (store, nodes, _) = setup_social_graph_with_ids();
768 let spec = ProjectionSpec::new().with_node_labels(["City"]);
769 let proj = GraphProjection::new(store, spec);
770
771 let epoch = EpochId(0);
772
773 assert!(proj.get_node_at_epoch(nodes[2], epoch).is_some());
775 assert!(proj.get_node_at_epoch(nodes[0], epoch).is_none());
777 }
778
779 #[test]
780 fn get_edge_at_epoch_respects_filter() {
781 let (store, _, edges) = setup_social_graph_with_ids();
782 let spec = ProjectionSpec::new().with_edge_types(["LIVES_IN"]);
783 let proj = GraphProjection::new(store, spec);
784
785 let epoch = EpochId(0);
786
787 assert!(proj.get_edge_at_epoch(edges[1], epoch).is_some());
789 assert!(proj.get_edge_at_epoch(edges[0], epoch).is_none());
791 }
792
793 #[test]
796 fn get_edge_property_in_projection() {
797 let (store, _, edges) = setup_social_graph_with_ids();
798 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
799 let proj = GraphProjection::new(store, spec);
800
801 let key = PropertyKey::from("since");
802 assert_eq!(
804 proj.get_edge_property(edges[0], &key),
805 Some(Value::from(2020))
806 );
807 }
808
809 #[test]
810 fn get_edge_property_outside_projection() {
811 let (store, _, edges) = setup_social_graph_with_ids();
812 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
813 let proj = GraphProjection::new(store, spec);
814
815 let key = PropertyKey::from("since");
816 assert!(proj.get_edge_property(edges[1], &key).is_none());
818 }
819
820 #[test]
823 fn get_node_property_batch_mixed() {
824 let (store, nodes, _) = setup_social_graph_with_ids();
825 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
826 let proj = GraphProjection::new(store, spec);
827
828 let key = PropertyKey::from("name");
829 let ids = vec![nodes[0], nodes[2], nodes[1]];
831 let results = proj.get_node_property_batch(&ids, &key);
832
833 assert_eq!(results.len(), 3);
834 assert_eq!(results[0], Some(Value::from("Alix"))); assert_eq!(results[1], None); assert_eq!(results[2], Some(Value::from("Gus"))); }
838
839 #[test]
842 fn get_nodes_properties_batch_filters() {
843 let (store, nodes, _) = setup_social_graph_with_ids();
844 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
845 let proj = GraphProjection::new(store, spec);
846
847 let ids = vec![nodes[0], nodes[2]]; let results = proj.get_nodes_properties_batch(&ids);
849
850 assert_eq!(results.len(), 2);
851 assert!(results[0].contains_key(&PropertyKey::from("name")));
853 assert!(results[1].is_empty());
855 }
856
857 #[test]
858 fn get_nodes_properties_selective_batch_filters() {
859 let (store, nodes, _) = setup_social_graph_with_ids();
860 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
861 let proj = GraphProjection::new(store, spec);
862
863 let ids = vec![nodes[0], nodes[2]]; let keys = vec![PropertyKey::from("name")];
865 let results = proj.get_nodes_properties_selective_batch(&ids, &keys);
866
867 assert_eq!(results.len(), 2);
868 assert_eq!(
869 results[0].get(&PropertyKey::from("name")),
870 Some(&Value::from("Alix"))
871 );
872 assert!(results[1].is_empty());
873 }
874
875 #[test]
878 fn get_edges_properties_selective_batch_filters() {
879 let (store, _, edges) = setup_social_graph_with_ids();
880 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
881 let proj = GraphProjection::new(store, spec);
882
883 let ids = vec![edges[0], edges[1]]; let keys = vec![PropertyKey::from("since")];
885 let results = proj.get_edges_properties_selective_batch(&ids, &keys);
886
887 assert_eq!(results.len(), 2);
888 assert_eq!(
890 results[0].get(&PropertyKey::from("since")),
891 Some(&Value::from(2020))
892 );
893 assert!(results[1].is_empty());
895 }
896
897 #[test]
900 fn edges_from_with_edge_type_filter() {
901 let (store, nodes, _) = setup_social_graph_with_ids();
902 let spec = ProjectionSpec::new().with_edge_types(["LIVES_IN"]);
903 let proj = GraphProjection::new(store, spec);
904
905 let alix_edges = proj.edges_from(nodes[0], Direction::Outgoing);
908 assert_eq!(alix_edges.len(), 1);
909 assert_eq!(alix_edges[0].0, nodes[2]); }
911
912 #[test]
913 fn edges_from_filtered_node_returns_empty() {
914 let (store, nodes, _) = setup_social_graph_with_ids();
915 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
916 let proj = GraphProjection::new(store, spec);
917
918 let amsterdam_edges = proj.edges_from(nodes[2], Direction::Outgoing);
920 assert!(amsterdam_edges.is_empty());
921 }
922
923 #[test]
926 fn out_degree_with_filter() {
927 let (store, nodes, _) = setup_social_graph_with_ids();
928 let spec = ProjectionSpec::new()
929 .with_node_labels(["Person", "City"])
930 .with_edge_types(["LIVES_IN"]);
931 let proj = GraphProjection::new(store, spec);
932
933 assert_eq!(proj.out_degree(nodes[0]), 1);
935 assert_eq!(proj.out_degree(nodes[1]), 1);
937 assert_eq!(proj.out_degree(nodes[2]), 0);
939 }
940
941 #[test]
942 fn in_degree_with_filter() {
943 let (store, nodes, _) = setup_social_graph_with_ids();
944 let spec = ProjectionSpec::new()
945 .with_node_labels(["Person", "City"])
946 .with_edge_types(["LIVES_IN"]);
947 let proj = GraphProjection::new(store, spec);
948
949 assert_eq!(proj.in_degree(nodes[2]), 2);
951 assert_eq!(proj.in_degree(nodes[0]), 0);
953 }
954
955 #[test]
958 fn all_node_ids_with_label_filter() {
959 let (store, nodes, _) = setup_social_graph_with_ids();
960 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
961 let proj = GraphProjection::new(store, spec);
962
963 let ids = proj.all_node_ids();
964 assert_eq!(ids.len(), 2);
965 assert!(ids.contains(&nodes[0])); assert!(ids.contains(&nodes[1])); assert!(!ids.contains(&nodes[2])); }
969
970 #[test]
971 fn all_node_ids_unfiltered() {
972 let (store, _, _) = setup_social_graph_with_ids();
973 let spec = ProjectionSpec::new();
974 let proj = GraphProjection::new(store.clone(), spec);
975
976 assert_eq!(proj.all_node_ids().len(), store.all_node_ids().len());
977 }
978
979 #[test]
982 fn node_count_with_city_filter() {
983 let (store, _, _) = setup_social_graph_with_ids();
984 let spec = ProjectionSpec::new().with_node_labels(["City"]);
985 let proj = GraphProjection::new(store, spec);
986
987 assert_eq!(proj.node_count(), 1);
988 }
989
990 #[test]
991 fn edge_count_with_combined_filter() {
992 let (store, _, _) = setup_social_graph_with_ids();
993 let spec = ProjectionSpec::new()
994 .with_node_labels(["Person"])
995 .with_edge_types(["KNOWS"]);
996 let proj = GraphProjection::new(store, spec);
997
998 assert_eq!(proj.edge_count(), 1);
1000 }
1001
1002 #[test]
1003 fn edge_count_unfiltered_delegates() {
1004 let (store, _, _) = setup_social_graph_with_ids();
1005 let spec = ProjectionSpec::new();
1006 let proj = GraphProjection::new(store.clone(), spec);
1007
1008 assert_eq!(proj.edge_count(), store.edge_count());
1009 }
1010
1011 #[test]
1014 fn find_nodes_by_property_with_label_filter() {
1015 let (store, nodes, _) = setup_social_graph_with_ids();
1016 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1017 let proj = GraphProjection::new(store, spec);
1018
1019 let found = proj.find_nodes_by_property("name", &Value::from("Alix"));
1021 assert_eq!(found.len(), 1);
1022 assert_eq!(found[0], nodes[0]);
1023
1024 let found = proj.find_nodes_by_property("name", &Value::from("Amsterdam"));
1026 assert!(found.is_empty());
1027 }
1028
1029 #[test]
1032 fn find_nodes_by_properties_with_label_filter() {
1033 let (store, nodes, _) = setup_social_graph_with_ids();
1034 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1035 let proj = GraphProjection::new(store, spec);
1036
1037 let conditions = vec![("name", Value::from("Gus"))];
1038 let found = proj.find_nodes_by_properties(&conditions);
1039 assert_eq!(found.len(), 1);
1040 assert_eq!(found[0], nodes[1]);
1041
1042 let conditions = vec![("name", Value::from("Amsterdam"))];
1044 let found = proj.find_nodes_by_properties(&conditions);
1045 assert!(found.is_empty());
1046 }
1047
1048 #[test]
1051 fn find_nodes_in_range_with_label_filter() {
1052 let (store, nodes, _) = setup_social_graph_with_ids();
1053 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1054 let proj = GraphProjection::new(store, spec);
1055
1056 let min = Value::from(20);
1058 let max = Value::from(30);
1059 let found = proj.find_nodes_in_range("age", Some(&min), Some(&max), true, true);
1060 assert_eq!(found.len(), 2);
1061 assert!(found.contains(&nodes[0])); assert!(found.contains(&nodes[1])); }
1064
1065 #[test]
1066 fn find_nodes_in_range_excludes_filtered_labels() {
1067 let (store, _, _) = setup_social_graph_with_ids();
1068 let spec = ProjectionSpec::new().with_node_labels(["City"]);
1069 let proj = GraphProjection::new(store, spec);
1070
1071 let min = Value::from(20);
1073 let max = Value::from(30);
1074 let found = proj.find_nodes_in_range("age", Some(&min), Some(&max), true, true);
1075 assert!(found.is_empty());
1076 }
1077
1078 #[test]
1081 fn node_property_might_match_delegates() {
1082 let (store, _, _) = setup_social_graph_with_ids();
1083 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1084 let proj = GraphProjection::new(store.clone(), spec);
1085
1086 let key = PropertyKey::from("name");
1087 let val = Value::from("Alix");
1088 let inner_result = store.node_property_might_match(&key, CompareOp::Eq, &val);
1090 assert_eq!(
1091 proj.node_property_might_match(&key, CompareOp::Eq, &val),
1092 inner_result
1093 );
1094 }
1095
1096 #[test]
1097 fn edge_property_might_match_delegates() {
1098 let (store, _, _) = setup_social_graph_with_ids();
1099 let spec = ProjectionSpec::new().with_edge_types(["KNOWS"]);
1100 let proj = GraphProjection::new(store.clone(), spec);
1101
1102 let key = PropertyKey::from("since");
1103 let val = Value::from(2020);
1104 let inner_result = store.edge_property_might_match(&key, CompareOp::Eq, &val);
1105 assert_eq!(
1106 proj.edge_property_might_match(&key, CompareOp::Eq, &val),
1107 inner_result
1108 );
1109 }
1110
1111 #[test]
1114 fn current_epoch_delegates() {
1115 let (store, _, _) = setup_social_graph_with_ids();
1116 let spec = ProjectionSpec::new();
1117 let proj = GraphProjection::new(store.clone(), spec);
1118
1119 assert_eq!(proj.current_epoch(), store.current_epoch());
1120 }
1121
1122 #[test]
1125 fn all_property_keys_delegates() {
1126 let (store, _, _) = setup_social_graph_with_ids();
1127 let spec = ProjectionSpec::new().with_node_labels(["Person"]);
1128 let proj = GraphProjection::new(store.clone(), spec);
1129
1130 let proj_keys = proj.all_property_keys();
1131 let store_keys = store.all_property_keys();
1132 assert_eq!(proj_keys.len(), store_keys.len());
1134 }
1135
1136 #[test]
1139 fn statistics_returns_value() {
1140 let (store, _, _) = setup_social_graph_with_ids();
1141 let spec = ProjectionSpec::new();
1142 let proj = GraphProjection::new(store, spec);
1143
1144 let stats = proj.statistics();
1145 let _ = stats;
1147 }
1148
1149 #[test]
1152 fn has_backward_adjacency_delegates() {
1153 let (store, _, _) = setup_social_graph_with_ids();
1154 let spec = ProjectionSpec::new();
1155 let proj = GraphProjection::new(store.clone(), spec);
1156
1157 assert_eq!(
1158 proj.has_backward_adjacency(),
1159 store.has_backward_adjacency()
1160 );
1161 }
1162
1163 #[test]
1166 fn has_property_index_delegates() {
1167 let (store, _, _) = setup_social_graph_with_ids();
1168 let spec = ProjectionSpec::new();
1169 let proj = GraphProjection::new(store.clone(), spec);
1170
1171 assert_eq!(
1173 proj.has_property_index("name"),
1174 store.has_property_index("name")
1175 );
1176 }
1177}