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