1use core_storage::v8::seam::{BaseColumns, ColumnsView, TopologyView};
38use core_storage::{ColumnStore, Direction, IdMap, Interner, Value};
39use serde::{Deserialize, Serialize};
40use std::collections::BTreeMap;
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub enum AggFn {
48 Sum,
49 Avg,
50 Min,
51 Max,
52 Count,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum ViewSource {
57 Degree {
58 edge_type: String,
59 direction: Direction,
60 },
61 NeighborAgg {
62 edge_type: String,
63 direction: Direction,
64 agg: AggFn,
65 prop: String,
67 },
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct ViewDef {
72 pub name: String,
73 pub label: String,
75 pub view_prop: String,
77 pub source: ViewSource,
78}
79
80impl ViewDef {
81 pub fn validate(&self) -> Result<(), String> {
82 if self.name.is_empty() {
83 return Err("view name must not be empty".into());
84 }
85 if self.label.is_empty() {
86 return Err("view label must not be empty".into());
87 }
88 if self.view_prop.is_empty() {
89 return Err("view_prop must not be empty".into());
90 }
91 if self.view_prop == core_storage::NS_PROP {
97 return Err(format!(
98 "view_prop must not be {:?}: it is the reserved namespace property, \
99 which is set at insert and never written again",
100 core_storage::NS_PROP
101 ));
102 }
103 match &self.source {
104 ViewSource::Degree { edge_type, .. } => {
105 if edge_type.is_empty() {
106 return Err("Degree edge_type must not be empty".into());
107 }
108 }
109 ViewSource::NeighborAgg {
110 edge_type, prop, ..
111 } => {
112 if edge_type.is_empty() {
113 return Err("NeighborAgg edge_type must not be empty".into());
114 }
115 if prop.is_empty() {
116 return Err("NeighborAgg prop must not be empty".into());
117 }
118 }
119 }
120 Ok(())
121 }
122
123 fn edge_type(&self) -> &str {
124 match &self.source {
125 ViewSource::Degree { edge_type, .. } => edge_type,
126 ViewSource::NeighborAgg { edge_type, .. } => edge_type,
127 }
128 }
129
130 fn direction(&self) -> Direction {
131 match &self.source {
132 ViewSource::Degree { direction, .. } => *direction,
133 ViewSource::NeighborAgg { direction, .. } => *direction,
134 }
135 }
136}
137
138#[derive(Debug, Default, Clone)]
143pub struct ViewStore {
144 views: BTreeMap<String, ViewDef>,
146}
147
148impl ViewStore {
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 pub fn views(&self) -> impl Iterator<Item = &ViewDef> {
154 self.views.values()
155 }
156
157 pub fn is_empty(&self) -> bool {
158 self.views.is_empty()
159 }
160
161 pub fn has_view(&self, name: &str) -> bool {
162 self.views.contains_key(name)
163 }
164
165 pub fn view_for_prop(&self, prop_name: &str) -> Option<&str> {
167 self.views
168 .values()
169 .find(|v| v.view_prop == prop_name)
170 .map(|v| v.name.as_str())
171 }
172
173 pub fn create_view(
184 &mut self,
185 def: ViewDef,
186 props: &mut ColumnStore,
187 topo: &TopologyView<'_>,
188 ids: &IdMap,
189 syms: &Interner,
190 labels: &[u32],
191 ) -> Result<(), String> {
192 self.create_view_over(def, props, topo, ids, syms, labels, None)
193 }
194
195 #[allow(clippy::too_many_arguments)]
200 pub fn create_view_over(
201 &mut self,
202 def: ViewDef,
203 props: &mut ColumnStore,
204 topo: &TopologyView<'_>,
205 ids: &IdMap,
206 syms: &Interner,
207 labels: &[u32],
208 base_cols: Option<BaseColumns<'_>>,
209 ) -> Result<(), String> {
210 def.validate()?;
211
212 if self.views.contains_key(&def.name) {
213 return Err(format!("view {:?} already exists", def.name));
214 }
215 if let Some(existing) = self.views.values().find(|v| v.view_prop == def.view_prop) {
216 return Err(format!(
217 "view_prop {:?} is already used by view {:?}",
218 def.view_prop, existing.name
219 ));
220 }
221 let view_props: std::collections::HashSet<&str> =
223 self.views.values().map(|v| v.view_prop.as_str()).collect();
224 let taken = props
230 .fields()
231 .any(|f| f == def.view_prop && !view_props.contains(f));
232 if taken {
233 return Err(format!(
234 "view_prop {:?} conflicts with an existing node property",
235 def.view_prop
236 ));
237 }
238
239 backfill_view(&def, props, topo, ids, syms, labels, base_cols);
241
242 self.views.insert(def.name.clone(), def);
243 Ok(())
244 }
245
246 pub fn restore_view(&mut self, def: ViewDef) -> Result<(), String> {
253 def.validate()?;
254 if self.views.contains_key(&def.name) {
255 return Ok(()); }
257 self.views.insert(def.name.clone(), def);
258 Ok(())
259 }
260
261 pub fn delete_view(
266 &mut self,
267 name: &str,
268 props: &mut ColumnStore,
269 ids: &IdMap,
270 labels: &[u32],
271 syms: &Interner,
272 ) -> Result<(), String> {
273 let def = self
274 .views
275 .remove(name)
276 .ok_or_else(|| format!("view {:?} not found", name))?;
277
278 if let Some(label_sym) = syms.get(&def.label) {
280 for id in 0..ids.len() as u32 {
281 if labels.get(id as usize).copied() == Some(label_sym) {
282 clear_view_value(props, id, &def.view_prop);
283 }
284 }
285 }
286 Ok(())
287 }
288
289 #[allow(clippy::too_many_arguments)]
301 pub fn on_edge_changed(
302 &self,
303 etype: u32,
304 src: u32,
305 dst: u32,
306 inserted: bool,
307 props: &mut ColumnStore,
308 topo: &TopologyView<'_>,
309 ids: &IdMap,
310 syms: &Interner,
311 labels: &[u32],
312 base_cols: Option<BaseColumns<'_>>,
313 ) {
314 self.on_edge_changed_except(
315 etype, src, dst, inserted, props, topo, ids, syms, labels, base_cols, None,
316 );
317 }
318
319 #[allow(clippy::too_many_arguments)]
324 pub fn on_edge_changed_except(
325 &self,
326 etype: u32,
327 src: u32,
328 dst: u32,
329 inserted: bool,
330 props: &mut ColumnStore,
331 topo: &TopologyView<'_>,
332 ids: &IdMap,
333 syms: &Interner,
334 labels: &[u32],
335 base_cols: Option<BaseColumns<'_>>,
336 gone: Option<u32>,
337 ) {
338 for def in self.views.values() {
339 let Some(et_sym) = syms.get(def.edge_type()) else {
340 continue;
341 };
342 if et_sym != etype {
343 continue;
344 }
345 let direction = def.direction();
346 let subject = match direction {
350 Direction::Out => src,
351 Direction::In => dst,
352 };
353 let neighbor = match direction {
355 Direction::Out => dst,
356 Direction::In => src,
357 };
358 if gone == Some(subject) {
359 continue;
360 }
361 update_node_view(
362 def, subject, neighbor, inserted, props, topo, ids, syms, labels, base_cols,
363 );
364 }
365 }
366
367 #[allow(clippy::too_many_arguments)]
370 pub fn on_prop_changed(
371 &self,
372 changed_node: u32,
373 field: &str,
374 props: &mut ColumnStore,
375 topo: &TopologyView<'_>,
376 ids: &IdMap,
377 syms: &Interner,
378 labels: &[u32],
379 base_cols: Option<BaseColumns<'_>>,
380 ) {
381 for def in self.views.values() {
382 let ViewSource::NeighborAgg {
383 edge_type,
384 direction,
385 prop,
386 ..
387 } = &def.source
388 else {
389 continue;
390 };
391 if prop != field {
392 continue;
393 }
394 let Some(et_sym) = syms.get(edge_type) else {
395 continue;
396 };
397 let reverse_dir = match direction {
401 Direction::Out => Direction::In,
402 Direction::In => Direction::Out,
403 };
404 let subjects: Vec<u32> = topo.neighbors(et_sym, reverse_dir, changed_node).to_vec();
405 for subject in subjects {
406 let val = {
410 let pv = build_cols_view(props, base_cols);
411 compute_view_value(def, subject, pv, topo, ids, syms, labels)
412 };
413 match val {
414 Some(v) => props.set(subject, &def.view_prop, v),
415 None => {
416 clear_view_value(props, subject, &def.view_prop);
417 }
418 }
419 }
420 }
421 }
422
423 pub fn init_node_views(
433 &self,
434 node: u32,
435 props: &mut ColumnStore,
436 syms: &Interner,
437 labels: &[u32],
438 ) {
439 for def in self.views.values() {
440 let Some(label_sym) = syms.get(&def.label) else {
441 continue;
442 };
443 if labels.get(node as usize).copied() != Some(label_sym) {
444 continue;
445 }
446 match &def.source {
447 ViewSource::Degree { .. } => {
448 if props.get(node, &def.view_prop).is_none() {
449 props.set(node, &def.view_prop, Value::Int(0));
450 }
451 }
452 ViewSource::NeighborAgg {
453 agg: AggFn::Count, ..
454 } => {
455 if props.get(node, &def.view_prop).is_none() {
456 props.set(node, &def.view_prop, Value::Int(0));
457 }
458 }
459 ViewSource::NeighborAgg {
460 agg: AggFn::Sum, ..
461 } => {
462 props.set(node, &def.view_prop, Value::Float(0.0));
465 }
466 _ => {} }
468 }
469 }
470
471 pub fn rebuild_all(
478 &self,
479 props: &mut ColumnStore,
480 topo: &TopologyView<'_>,
481 ids: &IdMap,
482 syms: &Interner,
483 labels: &[u32],
484 ) {
485 for def in self.views.values() {
486 backfill_view(def, props, topo, ids, syms, labels, None);
487 }
488 }
489}
490
491fn clear_view_value(props: &mut ColumnStore, node: u32, view_prop: &str) {
501 props.remove(node, view_prop);
502 props.record_prop_tombstone(node, view_prop);
503}
504
505fn build_cols_view<'a>(
510 overlay: &'a ColumnStore,
511 base_cols: Option<BaseColumns<'a>>,
512) -> ColumnsView<'a> {
513 match base_cols {
514 None => ColumnsView::owned(overlay),
515 Some(b) => ColumnsView::with_base(overlay, b.cols).with_shared_strings(b.strings),
518 }
519}
520
521fn backfill_view(
523 def: &ViewDef,
524 props: &mut ColumnStore,
525 topo: &TopologyView<'_>,
526 ids: &IdMap,
527 syms: &Interner,
528 labels: &[u32],
529 base_cols: Option<BaseColumns<'_>>,
530) {
531 let Some(label_sym) = syms.get(&def.label) else {
532 return;
533 };
534 let Some(et_sym) = syms.get(def.edge_type()) else {
535 for id in 0..ids.len() as u32 {
538 if labels.get(id as usize).copied() == Some(label_sym) {
539 match &def.source {
540 ViewSource::Degree { .. } => {
541 props.set(id, &def.view_prop, Value::Int(0));
542 }
543 ViewSource::NeighborAgg {
544 agg: AggFn::Count, ..
545 } => {
546 props.set(id, &def.view_prop, Value::Int(0));
547 }
548 ViewSource::NeighborAgg {
549 agg: AggFn::Sum, ..
550 } => {
551 props.set(id, &def.view_prop, Value::Float(0.0));
552 }
553 _ => {}
554 }
555 }
556 }
557 return;
558 };
559 for id in 0..ids.len() as u32 {
560 if labels.get(id as usize).copied() != Some(label_sym) {
561 continue;
562 }
563 let value = {
565 let pv = build_cols_view(props, base_cols);
566 compute_view_value(def, id, pv, topo, ids, syms, labels)
567 };
568 match value {
569 Some(val) => props.set(id, &def.view_prop, val),
570 None => {
571 clear_view_value(props, id, &def.view_prop);
572 }
575 }
576 let _ = et_sym;
580 }
581}
582
583pub fn compute_view_value(
586 def: &ViewDef,
587 node: u32,
588 props: ColumnsView<'_>,
589 topo: &TopologyView<'_>,
590 _ids: &IdMap,
591 syms: &Interner,
592 labels: &[u32],
593) -> Option<Value> {
594 let label_sym = syms.get(&def.label)?;
596 if labels.get(node as usize).copied() != Some(label_sym) {
597 return None;
598 }
599
600 let et_sym = match syms.get(def.edge_type()) {
604 Some(s) => s,
605 None => {
606 return match &def.source {
607 ViewSource::Degree { .. } => Some(Value::Int(0)),
608 ViewSource::NeighborAgg {
609 agg: AggFn::Count, ..
610 } => Some(Value::Int(0)),
611 ViewSource::NeighborAgg {
612 agg: AggFn::Sum, ..
613 } => Some(Value::Float(0.0)),
614 ViewSource::NeighborAgg { .. } => None,
615 };
616 }
617 };
618 let direction = def.direction();
619 let neighbors = topo.neighbors(et_sym, direction, node);
620
621 match &def.source {
622 ViewSource::Degree { .. } => Some(Value::Int(neighbors.len() as i64)),
623 ViewSource::NeighborAgg { agg, prop, .. } => match agg {
624 AggFn::Count => Some(Value::Int(
625 neighbors
626 .iter()
627 .filter(|&&n| props.get(n, prop).is_some())
628 .count() as i64,
629 )),
630 AggFn::Sum => {
631 let mut sum = 0.0f64;
632 for &nbr in neighbors.as_ref() {
633 if let Some(vr) = props.get(nbr, prop) {
634 if let Some(n) = as_float(vr.as_value()) {
635 sum += n;
636 }
637 }
638 }
639 Some(Value::Float(sum))
640 }
641 AggFn::Avg => {
642 let mut sum = 0.0f64;
643 let mut count = 0usize;
644 for &nbr in neighbors.as_ref() {
645 if let Some(vr) = props.get(nbr, prop) {
646 if let Some(n) = as_float(vr.as_value()) {
647 sum += n;
648 count += 1;
649 }
650 }
651 }
652 if count == 0 {
653 None
654 } else {
655 Some(Value::Float(sum / count as f64))
656 }
657 }
658 AggFn::Min => {
659 let mut best: Option<f64> = None;
660 for &nbr in neighbors.as_ref() {
661 if let Some(vr) = props.get(nbr, prop) {
662 if let Some(n) = as_float(vr.as_value()) {
663 best = Some(best.map_or(n, |m: f64| m.min(n)));
664 }
665 }
666 }
667 best.map(Value::Float)
668 }
669 AggFn::Max => {
670 let mut best: Option<f64> = None;
671 for &nbr in neighbors.as_ref() {
672 if let Some(vr) = props.get(nbr, prop) {
673 if let Some(n) = as_float(vr.as_value()) {
674 best = Some(best.map_or(n, |m: f64| m.max(n)));
675 }
676 }
677 }
678 best.map(Value::Float)
679 }
680 },
681 }
682}
683
684#[allow(clippy::too_many_arguments)]
692fn update_node_view(
693 def: &ViewDef,
694 subject: u32,
695 neighbor: u32,
696 inserted: bool,
697 props: &mut ColumnStore,
698 topo: &TopologyView<'_>,
699 ids: &IdMap,
700 syms: &Interner,
701 labels: &[u32],
702 base_cols: Option<BaseColumns<'_>>,
703) {
704 let Some(label_sym) = syms.get(&def.label) else {
706 return;
707 };
708 if labels.get(subject as usize).copied() != Some(label_sym) {
709 return;
710 }
711
712 match &def.source {
713 ViewSource::Degree { direction, .. } => {
714 let et_sym = match syms.get(def.edge_type()) {
717 Some(s) => s,
718 None => {
719 props.set(subject, &def.view_prop, Value::Int(0));
720 return;
721 }
722 };
723 let count = topo.neighbors(et_sym, *direction, subject).len() as i64;
724 props.set(subject, &def.view_prop, Value::Int(count));
725 }
726 ViewSource::NeighborAgg { .. } => {
727 let val = {
731 let pv = build_cols_view(props, base_cols);
732 compute_view_value(def, subject, pv, topo, ids, syms, labels)
733 };
734 match val {
735 Some(v) => props.set(subject, &def.view_prop, v),
736 None => {
737 clear_view_value(props, subject, &def.view_prop);
738 }
739 }
740 }
744 }
745 let _ = (neighbor, inserted); }
747
748fn as_float(v: &Value) -> Option<f64> {
749 match v {
750 Value::Int(i) => Some(*i as f64),
751 Value::Float(f) if f.is_finite() => Some(*f),
752 _ => None,
753 }
754}
755
756#[cfg(test)]
761mod tests {
762 use super::*;
763 use core_storage::{IdMap, Interner, Topology};
764
765 fn make_setup() -> (ViewStore, ColumnStore, Topology, IdMap, Interner, Vec<u32>) {
766 let mut ids = IdMap::new();
767 let mut syms = Interner::new();
768 let mut labels = Vec::new();
769 let mut topo = Topology::new();
770 let mut props = ColumnStore::new();
771
772 let person_sym = syms.intern("Person");
774 let city_sym = syms.intern("City");
775 let edge_sym = syms.intern("LIVES_IN");
776
777 for key in &["p0", "p1", "p2"] {
778 let id = ids.get_or_insert(key);
779 if labels.len() <= id as usize {
780 labels.resize(id as usize + 1, u32::MAX);
781 }
782 labels[id as usize] = person_sym;
783 }
784 let c0 = ids.get_or_insert("c0");
785 if labels.len() <= c0 as usize {
786 labels.resize(c0 as usize + 1, u32::MAX);
787 }
788 labels[c0 as usize] = city_sym;
789
790 let p0 = ids.get("p0").unwrap();
792 let p1 = ids.get("p1").unwrap();
793 topo.add_edge(edge_sym, p0, c0);
794 topo.add_edge(edge_sym, p1, c0);
795
796 props.set(p0, "score", Value::Float(3.0));
798 props.set(p1, "score", Value::Float(7.0));
799
800 (ViewStore::new(), props, topo, ids, syms, labels)
801 }
802
803 #[test]
804 fn degree_view_basic() {
805 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
806 let def = ViewDef {
807 name: "city_pop".into(),
808 label: "City".into(),
809 view_prop: "in_deg".into(),
810 source: ViewSource::Degree {
811 edge_type: "LIVES_IN".into(),
812 direction: Direction::In,
813 },
814 };
815 vs.create_view(
816 def,
817 &mut props,
818 &TopologyView::owned(&topo),
819 &ids,
820 &syms,
821 &labels,
822 )
823 .unwrap();
824 let c0 = ids.get("c0").unwrap();
825 assert_eq!(props.get(c0, "in_deg"), Some(&Value::Int(2)));
826 }
827
828 #[test]
829 fn neighbor_agg_sum() {
830 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
831 let def = ViewDef {
832 name: "city_score_sum".into(),
833 label: "City".into(),
834 view_prop: "score_sum".into(),
835 source: ViewSource::NeighborAgg {
836 edge_type: "LIVES_IN".into(),
837 direction: Direction::In,
838 agg: AggFn::Sum,
839 prop: "score".into(),
840 },
841 };
842 vs.create_view(
843 def,
844 &mut props,
845 &TopologyView::owned(&topo),
846 &ids,
847 &syms,
848 &labels,
849 )
850 .unwrap();
851 let c0 = ids.get("c0").unwrap();
852 assert_eq!(props.get(c0, "score_sum"), Some(&Value::Float(10.0)));
854 }
855
856 #[test]
857 fn neighbor_agg_count_skips_missing_prop() {
858 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
859 let p1 = ids.get("p1").unwrap();
860 props.remove(p1, "score");
861 let def = ViewDef {
862 name: "city_score_n".into(),
863 label: "City".into(),
864 view_prop: "score_n".into(),
865 source: ViewSource::NeighborAgg {
866 edge_type: "LIVES_IN".into(),
867 direction: Direction::In,
868 agg: AggFn::Count,
869 prop: "score".into(),
870 },
871 };
872 vs.create_view(
873 def,
874 &mut props,
875 &TopologyView::owned(&topo),
876 &ids,
877 &syms,
878 &labels,
879 )
880 .unwrap();
881 let c0 = ids.get("c0").unwrap();
882 assert_eq!(props.get(c0, "score_n"), Some(&Value::Int(1)));
883 }
884
885 #[test]
886 fn view_prop_collision_rejected() {
887 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
888 let p0 = ids.get("p0").unwrap();
890 props.set(p0, "collision_prop", Value::Int(1));
891 let def = ViewDef {
892 name: "test_view".into(),
893 label: "Person".into(),
894 view_prop: "collision_prop".into(),
895 source: ViewSource::Degree {
896 edge_type: "LIVES_IN".into(),
897 direction: Direction::Out,
898 },
899 };
900 let err = vs
901 .create_view(
902 def,
903 &mut props,
904 &TopologyView::owned(&topo),
905 &ids,
906 &syms,
907 &labels,
908 )
909 .unwrap_err();
910 assert!(
911 err.contains("conflicts with an existing node property"),
912 "{err}"
913 );
914 }
915
916 #[test]
917 fn delete_view_removes_values() {
918 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
919 let def = ViewDef {
920 name: "city_pop".into(),
921 label: "City".into(),
922 view_prop: "in_deg".into(),
923 source: ViewSource::Degree {
924 edge_type: "LIVES_IN".into(),
925 direction: Direction::In,
926 },
927 };
928 vs.create_view(
929 def,
930 &mut props,
931 &TopologyView::owned(&topo),
932 &ids,
933 &syms,
934 &labels,
935 )
936 .unwrap();
937 let c0 = ids.get("c0").unwrap();
938 assert!(props.get(c0, "in_deg").is_some());
939 vs.delete_view("city_pop", &mut props, &ids, &labels, &syms)
940 .unwrap();
941 assert!(props.get(c0, "in_deg").is_none());
942 }
943}