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 touched: Vec<u32>,
152}
153
154impl ViewStore {
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 pub fn views(&self) -> impl Iterator<Item = &ViewDef> {
160 self.views.values()
161 }
162
163 pub fn is_empty(&self) -> bool {
164 self.views.is_empty()
165 }
166
167 pub fn take_touched(&mut self) -> Vec<u32> {
170 std::mem::take(&mut self.touched)
171 }
172
173 pub fn clear_touched(&mut self) {
175 self.touched.clear();
176 }
177
178 pub fn has_view(&self, name: &str) -> bool {
179 self.views.contains_key(name)
180 }
181
182 pub fn view_for_prop(&self, prop_name: &str) -> Option<&str> {
184 self.views
185 .values()
186 .find(|v| v.view_prop == prop_name)
187 .map(|v| v.name.as_str())
188 }
189
190 pub fn create_view(
201 &mut self,
202 def: ViewDef,
203 props: &mut ColumnStore,
204 topo: &TopologyView<'_>,
205 ids: &IdMap,
206 syms: &Interner,
207 labels: &[u32],
208 ) -> Result<(), String> {
209 self.create_view_over(def, props, topo, ids, syms, labels, None)
210 }
211
212 #[allow(clippy::too_many_arguments)]
217 pub fn create_view_over(
218 &mut self,
219 def: ViewDef,
220 props: &mut ColumnStore,
221 topo: &TopologyView<'_>,
222 ids: &IdMap,
223 syms: &Interner,
224 labels: &[u32],
225 base_cols: Option<BaseColumns<'_>>,
226 ) -> Result<(), String> {
227 def.validate()?;
228
229 if self.views.contains_key(&def.name) {
230 return Err(format!("view {:?} already exists", def.name));
231 }
232 if let Some(existing) = self.views.values().find(|v| v.view_prop == def.view_prop) {
233 return Err(format!(
234 "view_prop {:?} is already used by view {:?}",
235 def.view_prop, existing.name
236 ));
237 }
238 let view_props: std::collections::HashSet<&str> =
240 self.views.values().map(|v| v.view_prop.as_str()).collect();
241 let taken = props
247 .fields()
248 .any(|f| f == def.view_prop && !view_props.contains(f));
249 if taken {
250 return Err(format!(
251 "view_prop {:?} conflicts with an existing node property",
252 def.view_prop
253 ));
254 }
255
256 backfill_view(&def, props, topo, ids, syms, labels, base_cols);
258
259 self.views.insert(def.name.clone(), def);
260 Ok(())
261 }
262
263 pub fn restore_view(&mut self, def: ViewDef) -> Result<(), String> {
270 def.validate()?;
271 if self.views.contains_key(&def.name) {
272 return Ok(()); }
274 self.views.insert(def.name.clone(), def);
275 Ok(())
276 }
277
278 pub fn delete_view(
283 &mut self,
284 name: &str,
285 props: &mut ColumnStore,
286 ids: &IdMap,
287 labels: &[u32],
288 syms: &Interner,
289 ) -> Result<(), String> {
290 let def = self
291 .views
292 .remove(name)
293 .ok_or_else(|| format!("view {:?} not found", name))?;
294
295 if let Some(label_sym) = syms.get(&def.label) {
297 for id in 0..ids.len() as u32 {
298 if labels.get(id as usize).copied() == Some(label_sym) {
299 clear_view_value(props, id, &def.view_prop);
300 }
301 }
302 }
303 Ok(())
304 }
305
306 #[allow(clippy::too_many_arguments)]
318 pub fn on_edge_changed(
319 &mut self,
320 etype: u32,
321 src: u32,
322 dst: u32,
323 inserted: bool,
324 props: &mut ColumnStore,
325 topo: &TopologyView<'_>,
326 ids: &IdMap,
327 syms: &Interner,
328 labels: &[u32],
329 base_cols: Option<BaseColumns<'_>>,
330 ) {
331 self.on_edge_changed_except(
332 etype, src, dst, inserted, props, topo, ids, syms, labels, base_cols, None,
333 );
334 }
335
336 #[allow(clippy::too_many_arguments)]
341 pub fn on_edge_changed_except(
342 &mut self,
343 etype: u32,
344 src: u32,
345 dst: u32,
346 inserted: bool,
347 props: &mut ColumnStore,
348 topo: &TopologyView<'_>,
349 ids: &IdMap,
350 syms: &Interner,
351 labels: &[u32],
352 base_cols: Option<BaseColumns<'_>>,
353 gone: Option<u32>,
354 ) {
355 for def in self.views.values() {
356 let Some(et_sym) = syms.get(def.edge_type()) else {
357 continue;
358 };
359 if et_sym != etype {
360 continue;
361 }
362 let direction = def.direction();
363 let subject = match direction {
367 Direction::Out => src,
368 Direction::In => dst,
369 };
370 let neighbor = match direction {
372 Direction::Out => dst,
373 Direction::In => src,
374 };
375 if gone == Some(subject) {
376 continue;
377 }
378 self.touched.push(subject);
379 update_node_view(
380 def, subject, neighbor, inserted, props, topo, ids, syms, labels, base_cols,
381 );
382 }
383 }
384
385 #[allow(clippy::too_many_arguments)]
388 pub fn on_prop_changed(
389 &mut self,
390 changed_node: u32,
391 field: &str,
392 props: &mut ColumnStore,
393 topo: &TopologyView<'_>,
394 ids: &IdMap,
395 syms: &Interner,
396 labels: &[u32],
397 base_cols: Option<BaseColumns<'_>>,
398 ) {
399 for def in self.views.values() {
400 let ViewSource::NeighborAgg {
401 edge_type,
402 direction,
403 prop,
404 ..
405 } = &def.source
406 else {
407 continue;
408 };
409 if prop != field {
410 continue;
411 }
412 let Some(et_sym) = syms.get(edge_type) else {
413 continue;
414 };
415 let reverse_dir = match direction {
419 Direction::Out => Direction::In,
420 Direction::In => Direction::Out,
421 };
422 let subjects: Vec<u32> = topo.neighbors(et_sym, reverse_dir, changed_node).to_vec();
423 self.touched.extend_from_slice(&subjects);
424 for subject in subjects {
425 let val = {
429 let pv = build_cols_view(props, base_cols);
430 compute_view_value(def, subject, pv, topo, ids, syms, labels)
431 };
432 match val {
433 Some(v) => props.set(subject, &def.view_prop, v),
434 None => {
435 clear_view_value(props, subject, &def.view_prop);
436 }
437 }
438 }
439 }
440 }
441
442 pub fn init_node_views(
452 &mut self,
453 node: u32,
454 props: &mut ColumnStore,
455 syms: &Interner,
456 labels: &[u32],
457 ) {
458 for def in self.views.values() {
459 let Some(label_sym) = syms.get(&def.label) else {
460 continue;
461 };
462 if labels.get(node as usize).copied() != Some(label_sym) {
463 continue;
464 }
465 self.touched.push(node);
466 match &def.source {
467 ViewSource::Degree { .. } => {
468 if props.get(node, &def.view_prop).is_none() {
469 props.set(node, &def.view_prop, Value::Int(0));
470 }
471 }
472 ViewSource::NeighborAgg {
473 agg: AggFn::Count, ..
474 } => {
475 if props.get(node, &def.view_prop).is_none() {
476 props.set(node, &def.view_prop, Value::Int(0));
477 }
478 }
479 ViewSource::NeighborAgg {
480 agg: AggFn::Sum, ..
481 } => {
482 props.set(node, &def.view_prop, Value::Float(0.0));
485 }
486 _ => {} }
488 }
489 }
490
491 pub fn rebuild_all(
498 &self,
499 props: &mut ColumnStore,
500 topo: &TopologyView<'_>,
501 ids: &IdMap,
502 syms: &Interner,
503 labels: &[u32],
504 ) {
505 for def in self.views.values() {
506 backfill_view(def, props, topo, ids, syms, labels, None);
507 }
508 }
509}
510
511fn clear_view_value(props: &mut ColumnStore, node: u32, view_prop: &str) {
521 props.remove(node, view_prop);
522 props.record_prop_tombstone(node, view_prop);
523}
524
525fn build_cols_view<'a>(
530 overlay: &'a ColumnStore,
531 base_cols: Option<BaseColumns<'a>>,
532) -> ColumnsView<'a> {
533 match base_cols {
534 None => ColumnsView::owned(overlay),
535 Some(b) => ColumnsView::with_base(overlay, b.cols).with_shared_strings(b.strings),
538 }
539}
540
541fn backfill_view(
543 def: &ViewDef,
544 props: &mut ColumnStore,
545 topo: &TopologyView<'_>,
546 ids: &IdMap,
547 syms: &Interner,
548 labels: &[u32],
549 base_cols: Option<BaseColumns<'_>>,
550) {
551 let Some(label_sym) = syms.get(&def.label) else {
552 return;
553 };
554 let Some(et_sym) = syms.get(def.edge_type()) else {
555 for id in 0..ids.len() as u32 {
558 if labels.get(id as usize).copied() == Some(label_sym) {
559 match &def.source {
560 ViewSource::Degree { .. } => {
561 props.set(id, &def.view_prop, Value::Int(0));
562 }
563 ViewSource::NeighborAgg {
564 agg: AggFn::Count, ..
565 } => {
566 props.set(id, &def.view_prop, Value::Int(0));
567 }
568 ViewSource::NeighborAgg {
569 agg: AggFn::Sum, ..
570 } => {
571 props.set(id, &def.view_prop, Value::Float(0.0));
572 }
573 _ => {}
574 }
575 }
576 }
577 return;
578 };
579 for id in 0..ids.len() as u32 {
580 if labels.get(id as usize).copied() != Some(label_sym) {
581 continue;
582 }
583 let value = {
585 let pv = build_cols_view(props, base_cols);
586 compute_view_value(def, id, pv, topo, ids, syms, labels)
587 };
588 match value {
589 Some(val) => props.set(id, &def.view_prop, val),
590 None => {
591 clear_view_value(props, id, &def.view_prop);
592 }
595 }
596 let _ = et_sym;
600 }
601}
602
603pub fn compute_view_value(
606 def: &ViewDef,
607 node: u32,
608 props: ColumnsView<'_>,
609 topo: &TopologyView<'_>,
610 _ids: &IdMap,
611 syms: &Interner,
612 labels: &[u32],
613) -> Option<Value> {
614 let label_sym = syms.get(&def.label)?;
616 if labels.get(node as usize).copied() != Some(label_sym) {
617 return None;
618 }
619
620 let et_sym = match syms.get(def.edge_type()) {
624 Some(s) => s,
625 None => {
626 return match &def.source {
627 ViewSource::Degree { .. } => Some(Value::Int(0)),
628 ViewSource::NeighborAgg {
629 agg: AggFn::Count, ..
630 } => Some(Value::Int(0)),
631 ViewSource::NeighborAgg {
632 agg: AggFn::Sum, ..
633 } => Some(Value::Float(0.0)),
634 ViewSource::NeighborAgg { .. } => None,
635 };
636 }
637 };
638 let direction = def.direction();
639 let neighbors = topo.neighbors(et_sym, direction, node);
640
641 match &def.source {
642 ViewSource::Degree { .. } => Some(Value::Int(neighbors.len() as i64)),
643 ViewSource::NeighborAgg { agg, prop, .. } => match agg {
644 AggFn::Count => Some(Value::Int(
645 neighbors
646 .iter()
647 .filter(|&&n| props.get(n, prop).is_some())
648 .count() as i64,
649 )),
650 AggFn::Sum => {
651 let mut sum = 0.0f64;
652 for &nbr in neighbors.as_ref() {
653 if let Some(vr) = props.get(nbr, prop) {
654 if let Some(n) = as_float(vr.as_value()) {
655 sum += n;
656 }
657 }
658 }
659 Some(Value::Float(sum))
660 }
661 AggFn::Avg => {
662 let mut sum = 0.0f64;
663 let mut count = 0usize;
664 for &nbr in neighbors.as_ref() {
665 if let Some(vr) = props.get(nbr, prop) {
666 if let Some(n) = as_float(vr.as_value()) {
667 sum += n;
668 count += 1;
669 }
670 }
671 }
672 if count == 0 {
673 None
674 } else {
675 Some(Value::Float(sum / count as f64))
676 }
677 }
678 AggFn::Min => {
679 let mut best: Option<f64> = None;
680 for &nbr in neighbors.as_ref() {
681 if let Some(vr) = props.get(nbr, prop) {
682 if let Some(n) = as_float(vr.as_value()) {
683 best = Some(best.map_or(n, |m: f64| m.min(n)));
684 }
685 }
686 }
687 best.map(Value::Float)
688 }
689 AggFn::Max => {
690 let mut best: Option<f64> = None;
691 for &nbr in neighbors.as_ref() {
692 if let Some(vr) = props.get(nbr, prop) {
693 if let Some(n) = as_float(vr.as_value()) {
694 best = Some(best.map_or(n, |m: f64| m.max(n)));
695 }
696 }
697 }
698 best.map(Value::Float)
699 }
700 },
701 }
702}
703
704#[allow(clippy::too_many_arguments)]
712fn update_node_view(
713 def: &ViewDef,
714 subject: u32,
715 neighbor: u32,
716 inserted: bool,
717 props: &mut ColumnStore,
718 topo: &TopologyView<'_>,
719 ids: &IdMap,
720 syms: &Interner,
721 labels: &[u32],
722 base_cols: Option<BaseColumns<'_>>,
723) {
724 let Some(label_sym) = syms.get(&def.label) else {
726 return;
727 };
728 if labels.get(subject as usize).copied() != Some(label_sym) {
729 return;
730 }
731
732 match &def.source {
733 ViewSource::Degree { direction, .. } => {
734 let et_sym = match syms.get(def.edge_type()) {
737 Some(s) => s,
738 None => {
739 props.set(subject, &def.view_prop, Value::Int(0));
740 return;
741 }
742 };
743 let count = topo.neighbors(et_sym, *direction, subject).len() as i64;
744 props.set(subject, &def.view_prop, Value::Int(count));
745 }
746 ViewSource::NeighborAgg { .. } => {
747 let val = {
751 let pv = build_cols_view(props, base_cols);
752 compute_view_value(def, subject, pv, topo, ids, syms, labels)
753 };
754 match val {
755 Some(v) => props.set(subject, &def.view_prop, v),
756 None => {
757 clear_view_value(props, subject, &def.view_prop);
758 }
759 }
760 }
764 }
765 let _ = (neighbor, inserted); }
767
768fn as_float(v: &Value) -> Option<f64> {
769 match v {
770 Value::Int(i) => Some(*i as f64),
771 Value::Float(f) if f.is_finite() => Some(*f),
772 _ => None,
773 }
774}
775
776#[cfg(test)]
781mod tests {
782 use super::*;
783 use core_storage::{IdMap, Interner, Topology};
784
785 fn make_setup() -> (ViewStore, ColumnStore, Topology, IdMap, Interner, Vec<u32>) {
786 let mut ids = IdMap::new();
787 let mut syms = Interner::new();
788 let mut labels = Vec::new();
789 let mut topo = Topology::new();
790 let mut props = ColumnStore::new();
791
792 let person_sym = syms.intern("Person");
794 let city_sym = syms.intern("City");
795 let edge_sym = syms.intern("LIVES_IN");
796
797 for key in &["p0", "p1", "p2"] {
798 let id = ids.get_or_insert(key);
799 if labels.len() <= id as usize {
800 labels.resize(id as usize + 1, u32::MAX);
801 }
802 labels[id as usize] = person_sym;
803 }
804 let c0 = ids.get_or_insert("c0");
805 if labels.len() <= c0 as usize {
806 labels.resize(c0 as usize + 1, u32::MAX);
807 }
808 labels[c0 as usize] = city_sym;
809
810 let p0 = ids.get("p0").unwrap();
812 let p1 = ids.get("p1").unwrap();
813 topo.add_edge(edge_sym, p0, c0);
814 topo.add_edge(edge_sym, p1, c0);
815
816 props.set(p0, "score", Value::Float(3.0));
818 props.set(p1, "score", Value::Float(7.0));
819
820 (ViewStore::new(), props, topo, ids, syms, labels)
821 }
822
823 #[test]
824 fn degree_view_basic() {
825 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
826 let def = ViewDef {
827 name: "city_pop".into(),
828 label: "City".into(),
829 view_prop: "in_deg".into(),
830 source: ViewSource::Degree {
831 edge_type: "LIVES_IN".into(),
832 direction: Direction::In,
833 },
834 };
835 vs.create_view(
836 def,
837 &mut props,
838 &TopologyView::owned(&topo),
839 &ids,
840 &syms,
841 &labels,
842 )
843 .unwrap();
844 let c0 = ids.get("c0").unwrap();
845 assert_eq!(props.get(c0, "in_deg"), Some(&Value::Int(2)));
846 }
847
848 #[test]
849 fn neighbor_agg_sum() {
850 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
851 let def = ViewDef {
852 name: "city_score_sum".into(),
853 label: "City".into(),
854 view_prop: "score_sum".into(),
855 source: ViewSource::NeighborAgg {
856 edge_type: "LIVES_IN".into(),
857 direction: Direction::In,
858 agg: AggFn::Sum,
859 prop: "score".into(),
860 },
861 };
862 vs.create_view(
863 def,
864 &mut props,
865 &TopologyView::owned(&topo),
866 &ids,
867 &syms,
868 &labels,
869 )
870 .unwrap();
871 let c0 = ids.get("c0").unwrap();
872 assert_eq!(props.get(c0, "score_sum"), Some(&Value::Float(10.0)));
874 }
875
876 #[test]
877 fn neighbor_agg_count_skips_missing_prop() {
878 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
879 let p1 = ids.get("p1").unwrap();
880 props.remove(p1, "score");
881 let def = ViewDef {
882 name: "city_score_n".into(),
883 label: "City".into(),
884 view_prop: "score_n".into(),
885 source: ViewSource::NeighborAgg {
886 edge_type: "LIVES_IN".into(),
887 direction: Direction::In,
888 agg: AggFn::Count,
889 prop: "score".into(),
890 },
891 };
892 vs.create_view(
893 def,
894 &mut props,
895 &TopologyView::owned(&topo),
896 &ids,
897 &syms,
898 &labels,
899 )
900 .unwrap();
901 let c0 = ids.get("c0").unwrap();
902 assert_eq!(props.get(c0, "score_n"), Some(&Value::Int(1)));
903 }
904
905 #[test]
906 fn view_prop_collision_rejected() {
907 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
908 let p0 = ids.get("p0").unwrap();
910 props.set(p0, "collision_prop", Value::Int(1));
911 let def = ViewDef {
912 name: "test_view".into(),
913 label: "Person".into(),
914 view_prop: "collision_prop".into(),
915 source: ViewSource::Degree {
916 edge_type: "LIVES_IN".into(),
917 direction: Direction::Out,
918 },
919 };
920 let err = vs
921 .create_view(
922 def,
923 &mut props,
924 &TopologyView::owned(&topo),
925 &ids,
926 &syms,
927 &labels,
928 )
929 .unwrap_err();
930 assert!(
931 err.contains("conflicts with an existing node property"),
932 "{err}"
933 );
934 }
935
936 #[test]
937 fn delete_view_removes_values() {
938 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
939 let def = ViewDef {
940 name: "city_pop".into(),
941 label: "City".into(),
942 view_prop: "in_deg".into(),
943 source: ViewSource::Degree {
944 edge_type: "LIVES_IN".into(),
945 direction: Direction::In,
946 },
947 };
948 vs.create_view(
949 def,
950 &mut props,
951 &TopologyView::owned(&topo),
952 &ids,
953 &syms,
954 &labels,
955 )
956 .unwrap();
957 let c0 = ids.get("c0").unwrap();
958 assert!(props.get(c0, "in_deg").is_some());
959 vs.delete_view("city_pop", &mut props, &ids, &labels, &syms)
960 .unwrap();
961 assert!(props.get(c0, "in_deg").is_none());
962 }
963}