1use core_storage::v8::layout::ArchivedColumns;
38use core_storage::v8::seam::{ColumnsView, TopologyView};
39use core_storage::{ColumnStore, Direction, IdMap, Interner, Value};
40use serde::{Deserialize, Serialize};
41use std::collections::BTreeMap;
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub enum AggFn {
49 Sum,
50 Avg,
51 Min,
52 Max,
53 Count,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub enum ViewSource {
58 Degree {
59 edge_type: String,
60 direction: Direction,
61 },
62 NeighborAgg {
63 edge_type: String,
64 direction: Direction,
65 agg: AggFn,
66 prop: String,
68 },
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct ViewDef {
73 pub name: String,
74 pub label: String,
76 pub view_prop: String,
78 pub source: ViewSource,
79}
80
81impl ViewDef {
82 pub fn validate(&self) -> Result<(), String> {
83 if self.name.is_empty() {
84 return Err("view name must not be empty".into());
85 }
86 if self.label.is_empty() {
87 return Err("view label must not be empty".into());
88 }
89 if self.view_prop.is_empty() {
90 return Err("view_prop must not be empty".into());
91 }
92 match &self.source {
93 ViewSource::Degree { edge_type, .. } => {
94 if edge_type.is_empty() {
95 return Err("Degree edge_type must not be empty".into());
96 }
97 }
98 ViewSource::NeighborAgg {
99 edge_type, prop, ..
100 } => {
101 if edge_type.is_empty() {
102 return Err("NeighborAgg edge_type must not be empty".into());
103 }
104 if prop.is_empty() {
105 return Err("NeighborAgg prop must not be empty".into());
106 }
107 }
108 }
109 Ok(())
110 }
111
112 fn edge_type(&self) -> &str {
113 match &self.source {
114 ViewSource::Degree { edge_type, .. } => edge_type,
115 ViewSource::NeighborAgg { edge_type, .. } => edge_type,
116 }
117 }
118
119 fn direction(&self) -> Direction {
120 match &self.source {
121 ViewSource::Degree { direction, .. } => *direction,
122 ViewSource::NeighborAgg { direction, .. } => *direction,
123 }
124 }
125}
126
127#[derive(Debug, Default, Clone)]
132pub struct ViewStore {
133 views: BTreeMap<String, ViewDef>,
135}
136
137impl ViewStore {
138 pub fn new() -> Self {
139 Self::default()
140 }
141
142 pub fn views(&self) -> impl Iterator<Item = &ViewDef> {
143 self.views.values()
144 }
145
146 pub fn is_empty(&self) -> bool {
147 self.views.is_empty()
148 }
149
150 pub fn has_view(&self, name: &str) -> bool {
151 self.views.contains_key(name)
152 }
153
154 pub fn view_for_prop(&self, prop_name: &str) -> Option<&str> {
156 self.views
157 .values()
158 .find(|v| v.view_prop == prop_name)
159 .map(|v| v.name.as_str())
160 }
161
162 pub fn create_view(
173 &mut self,
174 def: ViewDef,
175 props: &mut ColumnStore,
176 topo: &TopologyView<'_>,
177 ids: &IdMap,
178 syms: &Interner,
179 labels: &[u32],
180 ) -> Result<(), String> {
181 def.validate()?;
182
183 if self.views.contains_key(&def.name) {
184 return Err(format!("view {:?} already exists", def.name));
185 }
186 if let Some(existing) = self.views.values().find(|v| v.view_prop == def.view_prop) {
187 return Err(format!(
188 "view_prop {:?} is already used by view {:?}",
189 def.view_prop, existing.name
190 ));
191 }
192 let view_props: std::collections::HashSet<&str> =
194 self.views.values().map(|v| v.view_prop.as_str()).collect();
195 if props
196 .fields()
197 .any(|f| f == def.view_prop && !view_props.contains(f))
198 {
199 return Err(format!(
200 "view_prop {:?} conflicts with an existing node property",
201 def.view_prop
202 ));
203 }
204
205 backfill_view(&def, props, topo, ids, syms, labels);
207
208 self.views.insert(def.name.clone(), def);
209 Ok(())
210 }
211
212 pub fn restore_view(&mut self, def: ViewDef) -> Result<(), String> {
219 def.validate()?;
220 if self.views.contains_key(&def.name) {
221 return Ok(()); }
223 self.views.insert(def.name.clone(), def);
224 Ok(())
225 }
226
227 pub fn delete_view(
232 &mut self,
233 name: &str,
234 props: &mut ColumnStore,
235 ids: &IdMap,
236 labels: &[u32],
237 syms: &Interner,
238 ) -> Result<(), String> {
239 let def = self
240 .views
241 .remove(name)
242 .ok_or_else(|| format!("view {:?} not found", name))?;
243
244 if let Some(label_sym) = syms.get(&def.label) {
246 for id in 0..ids.len() as u32 {
247 if labels.get(id as usize).copied() == Some(label_sym) {
248 props.remove(id, &def.view_prop);
249 }
250 }
251 }
252 Ok(())
253 }
254
255 #[allow(clippy::too_many_arguments)]
267 pub fn on_edge_changed(
268 &self,
269 etype: u32,
270 src: u32,
271 dst: u32,
272 inserted: bool,
273 props: &mut ColumnStore,
274 topo: &TopologyView<'_>,
275 ids: &IdMap,
276 syms: &Interner,
277 labels: &[u32],
278 base_cols: Option<&ArchivedColumns>,
279 ) {
280 for def in self.views.values() {
281 let Some(et_sym) = syms.get(def.edge_type()) else {
282 continue;
283 };
284 if et_sym != etype {
285 continue;
286 }
287 let direction = def.direction();
288 let subject = match direction {
292 Direction::Out => src,
293 Direction::In => dst,
294 };
295 let neighbor = match direction {
297 Direction::Out => dst,
298 Direction::In => src,
299 };
300 update_node_view(
301 def, subject, neighbor, inserted, props, topo, ids, syms, labels, base_cols,
302 );
303 }
304 }
305
306 #[allow(clippy::too_many_arguments)]
309 pub fn on_prop_changed(
310 &self,
311 changed_node: u32,
312 field: &str,
313 props: &mut ColumnStore,
314 topo: &TopologyView<'_>,
315 ids: &IdMap,
316 syms: &Interner,
317 labels: &[u32],
318 base_cols: Option<&ArchivedColumns>,
319 ) {
320 for def in self.views.values() {
321 let ViewSource::NeighborAgg {
322 edge_type,
323 direction,
324 prop,
325 ..
326 } = &def.source
327 else {
328 continue;
329 };
330 if prop != field {
331 continue;
332 }
333 let Some(et_sym) = syms.get(edge_type) else {
334 continue;
335 };
336 let reverse_dir = match direction {
340 Direction::Out => Direction::In,
341 Direction::In => Direction::Out,
342 };
343 let subjects: Vec<u32> = topo.neighbors(et_sym, reverse_dir, changed_node).to_vec();
344 for subject in subjects {
345 let val = {
349 let pv = build_cols_view(props, base_cols);
350 compute_view_value(def, subject, pv, topo, ids, syms, labels)
351 };
352 match val {
353 Some(v) => props.set(subject, &def.view_prop, v),
354 None => {
355 props.remove(subject, &def.view_prop);
356 }
357 }
358 }
359 }
360 }
361
362 pub fn init_node_views(
372 &self,
373 node: u32,
374 props: &mut ColumnStore,
375 syms: &Interner,
376 labels: &[u32],
377 ) {
378 for def in self.views.values() {
379 let Some(label_sym) = syms.get(&def.label) else {
380 continue;
381 };
382 if labels.get(node as usize).copied() != Some(label_sym) {
383 continue;
384 }
385 match &def.source {
386 ViewSource::Degree { .. } => {
387 if props.get(node, &def.view_prop).is_none() {
388 props.set(node, &def.view_prop, Value::Int(0));
389 }
390 }
391 ViewSource::NeighborAgg {
392 agg: AggFn::Count, ..
393 } => {
394 if props.get(node, &def.view_prop).is_none() {
395 props.set(node, &def.view_prop, Value::Int(0));
396 }
397 }
398 ViewSource::NeighborAgg {
399 agg: AggFn::Sum, ..
400 } => {
401 props.set(node, &def.view_prop, Value::Float(0.0));
404 }
405 _ => {} }
407 }
408 }
409
410 pub fn rebuild_all(
417 &self,
418 props: &mut ColumnStore,
419 topo: &TopologyView<'_>,
420 ids: &IdMap,
421 syms: &Interner,
422 labels: &[u32],
423 ) {
424 for def in self.views.values() {
425 backfill_view(def, props, topo, ids, syms, labels);
426 }
427 }
428}
429
430fn build_cols_view<'a>(
439 overlay: &'a ColumnStore,
440 base_cols: Option<&'a ArchivedColumns>,
441) -> ColumnsView<'a> {
442 match base_cols {
443 None => ColumnsView::owned(overlay),
444 Some(b) => ColumnsView::with_base(overlay, b),
445 }
446}
447
448fn backfill_view(
450 def: &ViewDef,
451 props: &mut ColumnStore,
452 topo: &TopologyView<'_>,
453 ids: &IdMap,
454 syms: &Interner,
455 labels: &[u32],
456) {
457 let Some(label_sym) = syms.get(&def.label) else {
458 return;
459 };
460 let Some(et_sym) = syms.get(def.edge_type()) else {
461 for id in 0..ids.len() as u32 {
464 if labels.get(id as usize).copied() == Some(label_sym) {
465 match &def.source {
466 ViewSource::Degree { .. } => {
467 props.set(id, &def.view_prop, Value::Int(0));
468 }
469 ViewSource::NeighborAgg {
470 agg: AggFn::Count, ..
471 } => {
472 props.set(id, &def.view_prop, Value::Int(0));
473 }
474 ViewSource::NeighborAgg {
475 agg: AggFn::Sum, ..
476 } => {
477 props.set(id, &def.view_prop, Value::Float(0.0));
478 }
479 _ => {}
480 }
481 }
482 }
483 return;
484 };
485 for id in 0..ids.len() as u32 {
486 if labels.get(id as usize).copied() != Some(label_sym) {
487 continue;
488 }
489 match compute_view_value(
490 def,
491 id,
492 ColumnsView::owned(&*props),
493 topo,
494 ids,
495 syms,
496 labels,
497 ) {
498 Some(val) => props.set(id, &def.view_prop, val),
499 None => {
500 props.remove(id, &def.view_prop);
501 }
504 }
505 let _ = et_sym;
509 }
510}
511
512pub fn compute_view_value(
515 def: &ViewDef,
516 node: u32,
517 props: ColumnsView<'_>,
518 topo: &TopologyView<'_>,
519 _ids: &IdMap,
520 syms: &Interner,
521 labels: &[u32],
522) -> Option<Value> {
523 let label_sym = syms.get(&def.label)?;
525 if labels.get(node as usize).copied() != Some(label_sym) {
526 return None;
527 }
528
529 let et_sym = match syms.get(def.edge_type()) {
533 Some(s) => s,
534 None => {
535 return match &def.source {
536 ViewSource::Degree { .. } => Some(Value::Int(0)),
537 ViewSource::NeighborAgg {
538 agg: AggFn::Count, ..
539 } => Some(Value::Int(0)),
540 ViewSource::NeighborAgg {
541 agg: AggFn::Sum, ..
542 } => Some(Value::Float(0.0)),
543 ViewSource::NeighborAgg { .. } => None,
544 };
545 }
546 };
547 let direction = def.direction();
548 let neighbors = topo.neighbors(et_sym, direction, node);
549
550 match &def.source {
551 ViewSource::Degree { .. } => Some(Value::Int(neighbors.len() as i64)),
552 ViewSource::NeighborAgg { agg, prop, .. } => match agg {
553 AggFn::Count => Some(Value::Int(
554 neighbors
555 .iter()
556 .filter(|&&n| props.get(n, prop).is_some())
557 .count() as i64,
558 )),
559 AggFn::Sum => {
560 let mut sum = 0.0f64;
561 for &nbr in neighbors.as_ref() {
562 if let Some(vr) = props.get(nbr, prop) {
563 if let Some(n) = as_float(vr.as_value()) {
564 sum += n;
565 }
566 }
567 }
568 Some(Value::Float(sum))
569 }
570 AggFn::Avg => {
571 let mut sum = 0.0f64;
572 let mut count = 0usize;
573 for &nbr in neighbors.as_ref() {
574 if let Some(vr) = props.get(nbr, prop) {
575 if let Some(n) = as_float(vr.as_value()) {
576 sum += n;
577 count += 1;
578 }
579 }
580 }
581 if count == 0 {
582 None
583 } else {
584 Some(Value::Float(sum / count as f64))
585 }
586 }
587 AggFn::Min => {
588 let mut best: Option<f64> = None;
589 for &nbr in neighbors.as_ref() {
590 if let Some(vr) = props.get(nbr, prop) {
591 if let Some(n) = as_float(vr.as_value()) {
592 best = Some(best.map_or(n, |m: f64| m.min(n)));
593 }
594 }
595 }
596 best.map(Value::Float)
597 }
598 AggFn::Max => {
599 let mut best: Option<f64> = None;
600 for &nbr in neighbors.as_ref() {
601 if let Some(vr) = props.get(nbr, prop) {
602 if let Some(n) = as_float(vr.as_value()) {
603 best = Some(best.map_or(n, |m: f64| m.max(n)));
604 }
605 }
606 }
607 best.map(Value::Float)
608 }
609 },
610 }
611}
612
613#[allow(clippy::too_many_arguments)]
621fn update_node_view(
622 def: &ViewDef,
623 subject: u32,
624 neighbor: u32,
625 inserted: bool,
626 props: &mut ColumnStore,
627 topo: &TopologyView<'_>,
628 ids: &IdMap,
629 syms: &Interner,
630 labels: &[u32],
631 base_cols: Option<&ArchivedColumns>,
632) {
633 let Some(label_sym) = syms.get(&def.label) else {
635 return;
636 };
637 if labels.get(subject as usize).copied() != Some(label_sym) {
638 return;
639 }
640
641 match &def.source {
642 ViewSource::Degree { direction, .. } => {
643 let et_sym = match syms.get(def.edge_type()) {
646 Some(s) => s,
647 None => {
648 props.set(subject, &def.view_prop, Value::Int(0));
649 return;
650 }
651 };
652 let count = topo.neighbors(et_sym, *direction, subject).len() as i64;
653 props.set(subject, &def.view_prop, Value::Int(count));
654 }
655 ViewSource::NeighborAgg { .. } => {
656 let val = {
660 let pv = build_cols_view(props, base_cols);
661 compute_view_value(def, subject, pv, topo, ids, syms, labels)
662 };
663 match val {
664 Some(v) => props.set(subject, &def.view_prop, v),
665 None => {
666 props.remove(subject, &def.view_prop);
667 }
668 }
669 }
673 }
674 let _ = (neighbor, inserted); }
676
677fn as_float(v: &Value) -> Option<f64> {
678 match v {
679 Value::Int(i) => Some(*i as f64),
680 Value::Float(f) if f.is_finite() => Some(*f),
681 _ => None,
682 }
683}
684
685#[cfg(test)]
690mod tests {
691 use super::*;
692 use core_storage::{IdMap, Interner, Topology};
693
694 fn make_setup() -> (ViewStore, ColumnStore, Topology, IdMap, Interner, Vec<u32>) {
695 let mut ids = IdMap::new();
696 let mut syms = Interner::new();
697 let mut labels = Vec::new();
698 let mut topo = Topology::new();
699 let mut props = ColumnStore::new();
700
701 let person_sym = syms.intern("Person");
703 let city_sym = syms.intern("City");
704 let edge_sym = syms.intern("LIVES_IN");
705
706 for key in &["p0", "p1", "p2"] {
707 let id = ids.get_or_insert(key);
708 if labels.len() <= id as usize {
709 labels.resize(id as usize + 1, u32::MAX);
710 }
711 labels[id as usize] = person_sym;
712 }
713 let c0 = ids.get_or_insert("c0");
714 if labels.len() <= c0 as usize {
715 labels.resize(c0 as usize + 1, u32::MAX);
716 }
717 labels[c0 as usize] = city_sym;
718
719 let p0 = ids.get("p0").unwrap();
721 let p1 = ids.get("p1").unwrap();
722 topo.add_edge(edge_sym, p0, c0);
723 topo.add_edge(edge_sym, p1, c0);
724
725 props.set(p0, "score", Value::Float(3.0));
727 props.set(p1, "score", Value::Float(7.0));
728
729 (ViewStore::new(), props, topo, ids, syms, labels)
730 }
731
732 #[test]
733 fn degree_view_basic() {
734 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
735 let def = ViewDef {
736 name: "city_pop".into(),
737 label: "City".into(),
738 view_prop: "in_deg".into(),
739 source: ViewSource::Degree {
740 edge_type: "LIVES_IN".into(),
741 direction: Direction::In,
742 },
743 };
744 vs.create_view(
745 def,
746 &mut props,
747 &TopologyView::owned(&topo),
748 &ids,
749 &syms,
750 &labels,
751 )
752 .unwrap();
753 let c0 = ids.get("c0").unwrap();
754 assert_eq!(props.get(c0, "in_deg"), Some(&Value::Int(2)));
755 }
756
757 #[test]
758 fn neighbor_agg_sum() {
759 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
760 let def = ViewDef {
761 name: "city_score_sum".into(),
762 label: "City".into(),
763 view_prop: "score_sum".into(),
764 source: ViewSource::NeighborAgg {
765 edge_type: "LIVES_IN".into(),
766 direction: Direction::In,
767 agg: AggFn::Sum,
768 prop: "score".into(),
769 },
770 };
771 vs.create_view(
772 def,
773 &mut props,
774 &TopologyView::owned(&topo),
775 &ids,
776 &syms,
777 &labels,
778 )
779 .unwrap();
780 let c0 = ids.get("c0").unwrap();
781 assert_eq!(props.get(c0, "score_sum"), Some(&Value::Float(10.0)));
783 }
784
785 #[test]
786 fn neighbor_agg_count_skips_missing_prop() {
787 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
788 let p1 = ids.get("p1").unwrap();
789 props.remove(p1, "score");
790 let def = ViewDef {
791 name: "city_score_n".into(),
792 label: "City".into(),
793 view_prop: "score_n".into(),
794 source: ViewSource::NeighborAgg {
795 edge_type: "LIVES_IN".into(),
796 direction: Direction::In,
797 agg: AggFn::Count,
798 prop: "score".into(),
799 },
800 };
801 vs.create_view(
802 def,
803 &mut props,
804 &TopologyView::owned(&topo),
805 &ids,
806 &syms,
807 &labels,
808 )
809 .unwrap();
810 let c0 = ids.get("c0").unwrap();
811 assert_eq!(props.get(c0, "score_n"), Some(&Value::Int(1)));
812 }
813
814 #[test]
815 fn view_prop_collision_rejected() {
816 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
817 let p0 = ids.get("p0").unwrap();
819 props.set(p0, "collision_prop", Value::Int(1));
820 let def = ViewDef {
821 name: "test_view".into(),
822 label: "Person".into(),
823 view_prop: "collision_prop".into(),
824 source: ViewSource::Degree {
825 edge_type: "LIVES_IN".into(),
826 direction: Direction::Out,
827 },
828 };
829 let err = vs
830 .create_view(
831 def,
832 &mut props,
833 &TopologyView::owned(&topo),
834 &ids,
835 &syms,
836 &labels,
837 )
838 .unwrap_err();
839 assert!(
840 err.contains("conflicts with an existing node property"),
841 "{err}"
842 );
843 }
844
845 #[test]
846 fn delete_view_removes_values() {
847 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
848 let def = ViewDef {
849 name: "city_pop".into(),
850 label: "City".into(),
851 view_prop: "in_deg".into(),
852 source: ViewSource::Degree {
853 edge_type: "LIVES_IN".into(),
854 direction: Direction::In,
855 },
856 };
857 vs.create_view(
858 def,
859 &mut props,
860 &TopologyView::owned(&topo),
861 &ids,
862 &syms,
863 &labels,
864 )
865 .unwrap();
866 let c0 = ids.get("c0").unwrap();
867 assert!(props.get(c0, "in_deg").is_some());
868 vs.delete_view("city_pop", &mut props, &ids, &labels, &syms)
869 .unwrap();
870 assert!(props.get(c0, "in_deg").is_none());
871 }
872}