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 def.validate()?;
193
194 if self.views.contains_key(&def.name) {
195 return Err(format!("view {:?} already exists", def.name));
196 }
197 if let Some(existing) = self.views.values().find(|v| v.view_prop == def.view_prop) {
198 return Err(format!(
199 "view_prop {:?} is already used by view {:?}",
200 def.view_prop, existing.name
201 ));
202 }
203 let view_props: std::collections::HashSet<&str> =
205 self.views.values().map(|v| v.view_prop.as_str()).collect();
206 if props
207 .fields()
208 .any(|f| f == def.view_prop && !view_props.contains(f))
209 {
210 return Err(format!(
211 "view_prop {:?} conflicts with an existing node property",
212 def.view_prop
213 ));
214 }
215
216 backfill_view(&def, props, topo, ids, syms, labels);
218
219 self.views.insert(def.name.clone(), def);
220 Ok(())
221 }
222
223 pub fn restore_view(&mut self, def: ViewDef) -> Result<(), String> {
230 def.validate()?;
231 if self.views.contains_key(&def.name) {
232 return Ok(()); }
234 self.views.insert(def.name.clone(), def);
235 Ok(())
236 }
237
238 pub fn delete_view(
243 &mut self,
244 name: &str,
245 props: &mut ColumnStore,
246 ids: &IdMap,
247 labels: &[u32],
248 syms: &Interner,
249 ) -> Result<(), String> {
250 let def = self
251 .views
252 .remove(name)
253 .ok_or_else(|| format!("view {:?} not found", name))?;
254
255 if let Some(label_sym) = syms.get(&def.label) {
257 for id in 0..ids.len() as u32 {
258 if labels.get(id as usize).copied() == Some(label_sym) {
259 props.remove(id, &def.view_prop);
260 }
261 }
262 }
263 Ok(())
264 }
265
266 #[allow(clippy::too_many_arguments)]
278 pub fn on_edge_changed(
279 &self,
280 etype: u32,
281 src: u32,
282 dst: u32,
283 inserted: bool,
284 props: &mut ColumnStore,
285 topo: &TopologyView<'_>,
286 ids: &IdMap,
287 syms: &Interner,
288 labels: &[u32],
289 base_cols: Option<BaseColumns<'_>>,
290 ) {
291 for def in self.views.values() {
292 let Some(et_sym) = syms.get(def.edge_type()) else {
293 continue;
294 };
295 if et_sym != etype {
296 continue;
297 }
298 let direction = def.direction();
299 let subject = match direction {
303 Direction::Out => src,
304 Direction::In => dst,
305 };
306 let neighbor = match direction {
308 Direction::Out => dst,
309 Direction::In => src,
310 };
311 update_node_view(
312 def, subject, neighbor, inserted, props, topo, ids, syms, labels, base_cols,
313 );
314 }
315 }
316
317 #[allow(clippy::too_many_arguments)]
320 pub fn on_prop_changed(
321 &self,
322 changed_node: u32,
323 field: &str,
324 props: &mut ColumnStore,
325 topo: &TopologyView<'_>,
326 ids: &IdMap,
327 syms: &Interner,
328 labels: &[u32],
329 base_cols: Option<BaseColumns<'_>>,
330 ) {
331 for def in self.views.values() {
332 let ViewSource::NeighborAgg {
333 edge_type,
334 direction,
335 prop,
336 ..
337 } = &def.source
338 else {
339 continue;
340 };
341 if prop != field {
342 continue;
343 }
344 let Some(et_sym) = syms.get(edge_type) else {
345 continue;
346 };
347 let reverse_dir = match direction {
351 Direction::Out => Direction::In,
352 Direction::In => Direction::Out,
353 };
354 let subjects: Vec<u32> = topo.neighbors(et_sym, reverse_dir, changed_node).to_vec();
355 for subject in subjects {
356 let val = {
360 let pv = build_cols_view(props, base_cols);
361 compute_view_value(def, subject, pv, topo, ids, syms, labels)
362 };
363 match val {
364 Some(v) => props.set(subject, &def.view_prop, v),
365 None => {
366 props.remove(subject, &def.view_prop);
367 }
368 }
369 }
370 }
371 }
372
373 pub fn init_node_views(
383 &self,
384 node: u32,
385 props: &mut ColumnStore,
386 syms: &Interner,
387 labels: &[u32],
388 ) {
389 for def in self.views.values() {
390 let Some(label_sym) = syms.get(&def.label) else {
391 continue;
392 };
393 if labels.get(node as usize).copied() != Some(label_sym) {
394 continue;
395 }
396 match &def.source {
397 ViewSource::Degree { .. } => {
398 if props.get(node, &def.view_prop).is_none() {
399 props.set(node, &def.view_prop, Value::Int(0));
400 }
401 }
402 ViewSource::NeighborAgg {
403 agg: AggFn::Count, ..
404 } => {
405 if props.get(node, &def.view_prop).is_none() {
406 props.set(node, &def.view_prop, Value::Int(0));
407 }
408 }
409 ViewSource::NeighborAgg {
410 agg: AggFn::Sum, ..
411 } => {
412 props.set(node, &def.view_prop, Value::Float(0.0));
415 }
416 _ => {} }
418 }
419 }
420
421 pub fn rebuild_all(
428 &self,
429 props: &mut ColumnStore,
430 topo: &TopologyView<'_>,
431 ids: &IdMap,
432 syms: &Interner,
433 labels: &[u32],
434 ) {
435 for def in self.views.values() {
436 backfill_view(def, props, topo, ids, syms, labels);
437 }
438 }
439}
440
441fn build_cols_view<'a>(
450 overlay: &'a ColumnStore,
451 base_cols: Option<BaseColumns<'a>>,
452) -> ColumnsView<'a> {
453 match base_cols {
454 None => ColumnsView::owned(overlay),
455 Some(b) => ColumnsView::with_base(overlay, b.cols).with_shared_strings(b.strings),
458 }
459}
460
461fn backfill_view(
463 def: &ViewDef,
464 props: &mut ColumnStore,
465 topo: &TopologyView<'_>,
466 ids: &IdMap,
467 syms: &Interner,
468 labels: &[u32],
469) {
470 let Some(label_sym) = syms.get(&def.label) else {
471 return;
472 };
473 let Some(et_sym) = syms.get(def.edge_type()) else {
474 for id in 0..ids.len() as u32 {
477 if labels.get(id as usize).copied() == Some(label_sym) {
478 match &def.source {
479 ViewSource::Degree { .. } => {
480 props.set(id, &def.view_prop, Value::Int(0));
481 }
482 ViewSource::NeighborAgg {
483 agg: AggFn::Count, ..
484 } => {
485 props.set(id, &def.view_prop, Value::Int(0));
486 }
487 ViewSource::NeighborAgg {
488 agg: AggFn::Sum, ..
489 } => {
490 props.set(id, &def.view_prop, Value::Float(0.0));
491 }
492 _ => {}
493 }
494 }
495 }
496 return;
497 };
498 for id in 0..ids.len() as u32 {
499 if labels.get(id as usize).copied() != Some(label_sym) {
500 continue;
501 }
502 match compute_view_value(
503 def,
504 id,
505 ColumnsView::owned(&*props),
506 topo,
507 ids,
508 syms,
509 labels,
510 ) {
511 Some(val) => props.set(id, &def.view_prop, val),
512 None => {
513 props.remove(id, &def.view_prop);
514 }
517 }
518 let _ = et_sym;
522 }
523}
524
525pub fn compute_view_value(
528 def: &ViewDef,
529 node: u32,
530 props: ColumnsView<'_>,
531 topo: &TopologyView<'_>,
532 _ids: &IdMap,
533 syms: &Interner,
534 labels: &[u32],
535) -> Option<Value> {
536 let label_sym = syms.get(&def.label)?;
538 if labels.get(node as usize).copied() != Some(label_sym) {
539 return None;
540 }
541
542 let et_sym = match syms.get(def.edge_type()) {
546 Some(s) => s,
547 None => {
548 return match &def.source {
549 ViewSource::Degree { .. } => Some(Value::Int(0)),
550 ViewSource::NeighborAgg {
551 agg: AggFn::Count, ..
552 } => Some(Value::Int(0)),
553 ViewSource::NeighborAgg {
554 agg: AggFn::Sum, ..
555 } => Some(Value::Float(0.0)),
556 ViewSource::NeighborAgg { .. } => None,
557 };
558 }
559 };
560 let direction = def.direction();
561 let neighbors = topo.neighbors(et_sym, direction, node);
562
563 match &def.source {
564 ViewSource::Degree { .. } => Some(Value::Int(neighbors.len() as i64)),
565 ViewSource::NeighborAgg { agg, prop, .. } => match agg {
566 AggFn::Count => Some(Value::Int(
567 neighbors
568 .iter()
569 .filter(|&&n| props.get(n, prop).is_some())
570 .count() as i64,
571 )),
572 AggFn::Sum => {
573 let mut sum = 0.0f64;
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 }
579 }
580 }
581 Some(Value::Float(sum))
582 }
583 AggFn::Avg => {
584 let mut sum = 0.0f64;
585 let mut count = 0usize;
586 for &nbr in neighbors.as_ref() {
587 if let Some(vr) = props.get(nbr, prop) {
588 if let Some(n) = as_float(vr.as_value()) {
589 sum += n;
590 count += 1;
591 }
592 }
593 }
594 if count == 0 {
595 None
596 } else {
597 Some(Value::Float(sum / count as f64))
598 }
599 }
600 AggFn::Min => {
601 let mut best: Option<f64> = None;
602 for &nbr in neighbors.as_ref() {
603 if let Some(vr) = props.get(nbr, prop) {
604 if let Some(n) = as_float(vr.as_value()) {
605 best = Some(best.map_or(n, |m: f64| m.min(n)));
606 }
607 }
608 }
609 best.map(Value::Float)
610 }
611 AggFn::Max => {
612 let mut best: Option<f64> = None;
613 for &nbr in neighbors.as_ref() {
614 if let Some(vr) = props.get(nbr, prop) {
615 if let Some(n) = as_float(vr.as_value()) {
616 best = Some(best.map_or(n, |m: f64| m.max(n)));
617 }
618 }
619 }
620 best.map(Value::Float)
621 }
622 },
623 }
624}
625
626#[allow(clippy::too_many_arguments)]
634fn update_node_view(
635 def: &ViewDef,
636 subject: u32,
637 neighbor: u32,
638 inserted: bool,
639 props: &mut ColumnStore,
640 topo: &TopologyView<'_>,
641 ids: &IdMap,
642 syms: &Interner,
643 labels: &[u32],
644 base_cols: Option<BaseColumns<'_>>,
645) {
646 let Some(label_sym) = syms.get(&def.label) else {
648 return;
649 };
650 if labels.get(subject as usize).copied() != Some(label_sym) {
651 return;
652 }
653
654 match &def.source {
655 ViewSource::Degree { direction, .. } => {
656 let et_sym = match syms.get(def.edge_type()) {
659 Some(s) => s,
660 None => {
661 props.set(subject, &def.view_prop, Value::Int(0));
662 return;
663 }
664 };
665 let count = topo.neighbors(et_sym, *direction, subject).len() as i64;
666 props.set(subject, &def.view_prop, Value::Int(count));
667 }
668 ViewSource::NeighborAgg { .. } => {
669 let val = {
673 let pv = build_cols_view(props, base_cols);
674 compute_view_value(def, subject, pv, topo, ids, syms, labels)
675 };
676 match val {
677 Some(v) => props.set(subject, &def.view_prop, v),
678 None => {
679 props.remove(subject, &def.view_prop);
680 }
681 }
682 }
686 }
687 let _ = (neighbor, inserted); }
689
690fn as_float(v: &Value) -> Option<f64> {
691 match v {
692 Value::Int(i) => Some(*i as f64),
693 Value::Float(f) if f.is_finite() => Some(*f),
694 _ => None,
695 }
696}
697
698#[cfg(test)]
703mod tests {
704 use super::*;
705 use core_storage::{IdMap, Interner, Topology};
706
707 fn make_setup() -> (ViewStore, ColumnStore, Topology, IdMap, Interner, Vec<u32>) {
708 let mut ids = IdMap::new();
709 let mut syms = Interner::new();
710 let mut labels = Vec::new();
711 let mut topo = Topology::new();
712 let mut props = ColumnStore::new();
713
714 let person_sym = syms.intern("Person");
716 let city_sym = syms.intern("City");
717 let edge_sym = syms.intern("LIVES_IN");
718
719 for key in &["p0", "p1", "p2"] {
720 let id = ids.get_or_insert(key);
721 if labels.len() <= id as usize {
722 labels.resize(id as usize + 1, u32::MAX);
723 }
724 labels[id as usize] = person_sym;
725 }
726 let c0 = ids.get_or_insert("c0");
727 if labels.len() <= c0 as usize {
728 labels.resize(c0 as usize + 1, u32::MAX);
729 }
730 labels[c0 as usize] = city_sym;
731
732 let p0 = ids.get("p0").unwrap();
734 let p1 = ids.get("p1").unwrap();
735 topo.add_edge(edge_sym, p0, c0);
736 topo.add_edge(edge_sym, p1, c0);
737
738 props.set(p0, "score", Value::Float(3.0));
740 props.set(p1, "score", Value::Float(7.0));
741
742 (ViewStore::new(), props, topo, ids, syms, labels)
743 }
744
745 #[test]
746 fn degree_view_basic() {
747 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
748 let def = ViewDef {
749 name: "city_pop".into(),
750 label: "City".into(),
751 view_prop: "in_deg".into(),
752 source: ViewSource::Degree {
753 edge_type: "LIVES_IN".into(),
754 direction: Direction::In,
755 },
756 };
757 vs.create_view(
758 def,
759 &mut props,
760 &TopologyView::owned(&topo),
761 &ids,
762 &syms,
763 &labels,
764 )
765 .unwrap();
766 let c0 = ids.get("c0").unwrap();
767 assert_eq!(props.get(c0, "in_deg"), Some(&Value::Int(2)));
768 }
769
770 #[test]
771 fn neighbor_agg_sum() {
772 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
773 let def = ViewDef {
774 name: "city_score_sum".into(),
775 label: "City".into(),
776 view_prop: "score_sum".into(),
777 source: ViewSource::NeighborAgg {
778 edge_type: "LIVES_IN".into(),
779 direction: Direction::In,
780 agg: AggFn::Sum,
781 prop: "score".into(),
782 },
783 };
784 vs.create_view(
785 def,
786 &mut props,
787 &TopologyView::owned(&topo),
788 &ids,
789 &syms,
790 &labels,
791 )
792 .unwrap();
793 let c0 = ids.get("c0").unwrap();
794 assert_eq!(props.get(c0, "score_sum"), Some(&Value::Float(10.0)));
796 }
797
798 #[test]
799 fn neighbor_agg_count_skips_missing_prop() {
800 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
801 let p1 = ids.get("p1").unwrap();
802 props.remove(p1, "score");
803 let def = ViewDef {
804 name: "city_score_n".into(),
805 label: "City".into(),
806 view_prop: "score_n".into(),
807 source: ViewSource::NeighborAgg {
808 edge_type: "LIVES_IN".into(),
809 direction: Direction::In,
810 agg: AggFn::Count,
811 prop: "score".into(),
812 },
813 };
814 vs.create_view(
815 def,
816 &mut props,
817 &TopologyView::owned(&topo),
818 &ids,
819 &syms,
820 &labels,
821 )
822 .unwrap();
823 let c0 = ids.get("c0").unwrap();
824 assert_eq!(props.get(c0, "score_n"), Some(&Value::Int(1)));
825 }
826
827 #[test]
828 fn view_prop_collision_rejected() {
829 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
830 let p0 = ids.get("p0").unwrap();
832 props.set(p0, "collision_prop", Value::Int(1));
833 let def = ViewDef {
834 name: "test_view".into(),
835 label: "Person".into(),
836 view_prop: "collision_prop".into(),
837 source: ViewSource::Degree {
838 edge_type: "LIVES_IN".into(),
839 direction: Direction::Out,
840 },
841 };
842 let err = vs
843 .create_view(
844 def,
845 &mut props,
846 &TopologyView::owned(&topo),
847 &ids,
848 &syms,
849 &labels,
850 )
851 .unwrap_err();
852 assert!(
853 err.contains("conflicts with an existing node property"),
854 "{err}"
855 );
856 }
857
858 #[test]
859 fn delete_view_removes_values() {
860 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
861 let def = ViewDef {
862 name: "city_pop".into(),
863 label: "City".into(),
864 view_prop: "in_deg".into(),
865 source: ViewSource::Degree {
866 edge_type: "LIVES_IN".into(),
867 direction: Direction::In,
868 },
869 };
870 vs.create_view(
871 def,
872 &mut props,
873 &TopologyView::owned(&topo),
874 &ids,
875 &syms,
876 &labels,
877 )
878 .unwrap();
879 let c0 = ids.get("c0").unwrap();
880 assert!(props.get(c0, "in_deg").is_some());
881 vs.delete_view("city_pop", &mut props, &ids, &labels, &syms)
882 .unwrap();
883 assert!(props.get(c0, "in_deg").is_none());
884 }
885}