1use core_storage::{ColumnStore, Direction, IdMap, Interner, Topology, Value};
38use serde::{Deserialize, Serialize};
39use std::collections::BTreeMap;
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub enum AggFn {
47 Sum,
48 Avg,
49 Min,
50 Max,
51 Count,
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub enum ViewSource {
56 Degree {
57 edge_type: String,
58 direction: Direction,
59 },
60 NeighborAgg {
61 edge_type: String,
62 direction: Direction,
63 agg: AggFn,
64 prop: String,
66 },
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct ViewDef {
71 pub name: String,
72 pub label: String,
74 pub view_prop: String,
76 pub source: ViewSource,
77}
78
79impl ViewDef {
80 pub fn validate(&self) -> Result<(), String> {
81 if self.name.is_empty() {
82 return Err("view name must not be empty".into());
83 }
84 if self.label.is_empty() {
85 return Err("view label must not be empty".into());
86 }
87 if self.view_prop.is_empty() {
88 return Err("view_prop must not be empty".into());
89 }
90 match &self.source {
91 ViewSource::Degree { edge_type, .. } => {
92 if edge_type.is_empty() {
93 return Err("Degree edge_type must not be empty".into());
94 }
95 }
96 ViewSource::NeighborAgg {
97 edge_type, prop, ..
98 } => {
99 if edge_type.is_empty() {
100 return Err("NeighborAgg edge_type must not be empty".into());
101 }
102 if prop.is_empty() {
103 return Err("NeighborAgg prop must not be empty".into());
104 }
105 }
106 }
107 Ok(())
108 }
109
110 fn edge_type(&self) -> &str {
111 match &self.source {
112 ViewSource::Degree { edge_type, .. } => edge_type,
113 ViewSource::NeighborAgg { edge_type, .. } => edge_type,
114 }
115 }
116
117 fn direction(&self) -> Direction {
118 match &self.source {
119 ViewSource::Degree { direction, .. } => *direction,
120 ViewSource::NeighborAgg { direction, .. } => *direction,
121 }
122 }
123}
124
125#[derive(Debug, Default, Clone)]
130pub struct ViewStore {
131 views: BTreeMap<String, ViewDef>,
133}
134
135impl ViewStore {
136 pub fn new() -> Self {
137 Self::default()
138 }
139
140 pub fn views(&self) -> impl Iterator<Item = &ViewDef> {
141 self.views.values()
142 }
143
144 pub fn is_empty(&self) -> bool {
145 self.views.is_empty()
146 }
147
148 pub fn has_view(&self, name: &str) -> bool {
149 self.views.contains_key(name)
150 }
151
152 pub fn view_for_prop(&self, prop_name: &str) -> Option<&str> {
154 self.views
155 .values()
156 .find(|v| v.view_prop == prop_name)
157 .map(|v| v.name.as_str())
158 }
159
160 pub fn create_view(
171 &mut self,
172 def: ViewDef,
173 props: &mut ColumnStore,
174 topo: &Topology,
175 ids: &IdMap,
176 syms: &Interner,
177 labels: &[u32],
178 ) -> Result<(), String> {
179 def.validate()?;
180
181 if self.views.contains_key(&def.name) {
182 return Err(format!("view {:?} already exists", def.name));
183 }
184 if let Some(existing) = self.views.values().find(|v| v.view_prop == def.view_prop) {
185 return Err(format!(
186 "view_prop {:?} is already used by view {:?}",
187 def.view_prop, existing.name
188 ));
189 }
190 let view_props: std::collections::HashSet<&str> =
192 self.views.values().map(|v| v.view_prop.as_str()).collect();
193 if props
194 .fields()
195 .any(|f| f == def.view_prop && !view_props.contains(f))
196 {
197 return Err(format!(
198 "view_prop {:?} conflicts with an existing node property",
199 def.view_prop
200 ));
201 }
202
203 backfill_view(&def, props, topo, ids, syms, labels);
205
206 self.views.insert(def.name.clone(), def);
207 Ok(())
208 }
209
210 pub fn restore_view(&mut self, def: ViewDef) -> Result<(), String> {
217 def.validate()?;
218 if self.views.contains_key(&def.name) {
219 return Ok(()); }
221 self.views.insert(def.name.clone(), def);
222 Ok(())
223 }
224
225 pub fn delete_view(
230 &mut self,
231 name: &str,
232 props: &mut ColumnStore,
233 ids: &IdMap,
234 labels: &[u32],
235 syms: &Interner,
236 ) -> Result<(), String> {
237 let def = self
238 .views
239 .remove(name)
240 .ok_or_else(|| format!("view {:?} not found", name))?;
241
242 if let Some(label_sym) = syms.get(&def.label) {
244 for id in 0..ids.len() as u32 {
245 if labels.get(id as usize).copied() == Some(label_sym) {
246 props.remove(id, &def.view_prop);
247 }
248 }
249 }
250 Ok(())
251 }
252
253 #[allow(clippy::too_many_arguments)]
265 pub fn on_edge_changed(
266 &self,
267 etype: u32,
268 src: u32,
269 dst: u32,
270 inserted: bool,
271 props: &mut ColumnStore,
272 topo: &Topology,
273 ids: &IdMap,
274 syms: &Interner,
275 labels: &[u32],
276 ) {
277 for def in self.views.values() {
278 let Some(et_sym) = syms.get(def.edge_type()) else {
279 continue;
280 };
281 if et_sym != etype {
282 continue;
283 }
284 let direction = def.direction();
285 let subject = match direction {
289 Direction::Out => src,
290 Direction::In => dst,
291 };
292 let neighbor = match direction {
294 Direction::Out => dst,
295 Direction::In => src,
296 };
297 update_node_view(
298 def, subject, neighbor, inserted, props, topo, ids, syms, labels,
299 );
300 }
301 }
302
303 #[allow(clippy::too_many_arguments)]
306 pub fn on_prop_changed(
307 &self,
308 changed_node: u32,
309 field: &str,
310 props: &mut ColumnStore,
311 topo: &Topology,
312 ids: &IdMap,
313 syms: &Interner,
314 labels: &[u32],
315 ) {
316 for def in self.views.values() {
317 let ViewSource::NeighborAgg {
318 edge_type,
319 direction,
320 prop,
321 ..
322 } = &def.source
323 else {
324 continue;
325 };
326 if prop != field {
327 continue;
328 }
329 let Some(et_sym) = syms.get(edge_type) else {
330 continue;
331 };
332 let reverse_dir = match direction {
336 Direction::Out => Direction::In,
337 Direction::In => Direction::Out,
338 };
339 let subjects: Vec<u32> = topo.neighbors(et_sym, reverse_dir, changed_node).to_vec();
340 for subject in subjects {
341 if let Some(val) = compute_view_value(def, subject, props, topo, ids, syms, labels)
343 {
344 props.set(subject, &def.view_prop, val);
345 } else {
346 props.remove(subject, &def.view_prop);
347 }
348 }
349 }
350 }
351
352 pub fn init_node_views(
362 &self,
363 node: u32,
364 props: &mut ColumnStore,
365 syms: &Interner,
366 labels: &[u32],
367 ) {
368 for def in self.views.values() {
369 let Some(label_sym) = syms.get(&def.label) else {
370 continue;
371 };
372 if labels.get(node as usize).copied() != Some(label_sym) {
373 continue;
374 }
375 match &def.source {
376 ViewSource::Degree { .. } => {
377 if props.get(node, &def.view_prop).is_none() {
378 props.set(node, &def.view_prop, Value::Int(0));
379 }
380 }
381 ViewSource::NeighborAgg {
382 agg: AggFn::Count, ..
383 } => {
384 if props.get(node, &def.view_prop).is_none() {
385 props.set(node, &def.view_prop, Value::Int(0));
386 }
387 }
388 ViewSource::NeighborAgg {
389 agg: AggFn::Sum, ..
390 } => {
391 props.set(node, &def.view_prop, Value::Float(0.0));
394 }
395 _ => {} }
397 }
398 }
399
400 pub fn rebuild_all(
407 &self,
408 props: &mut ColumnStore,
409 topo: &Topology,
410 ids: &IdMap,
411 syms: &Interner,
412 labels: &[u32],
413 ) {
414 for def in self.views.values() {
415 backfill_view(def, props, topo, ids, syms, labels);
416 }
417 }
418}
419
420fn backfill_view(
426 def: &ViewDef,
427 props: &mut ColumnStore,
428 topo: &Topology,
429 ids: &IdMap,
430 syms: &Interner,
431 labels: &[u32],
432) {
433 let Some(label_sym) = syms.get(&def.label) else {
434 return;
435 };
436 let Some(et_sym) = syms.get(def.edge_type()) else {
437 for id in 0..ids.len() as u32 {
440 if labels.get(id as usize).copied() == Some(label_sym) {
441 match &def.source {
442 ViewSource::Degree { .. } => {
443 props.set(id, &def.view_prop, Value::Int(0));
444 }
445 ViewSource::NeighborAgg {
446 agg: AggFn::Count, ..
447 } => {
448 props.set(id, &def.view_prop, Value::Int(0));
449 }
450 ViewSource::NeighborAgg {
451 agg: AggFn::Sum, ..
452 } => {
453 props.set(id, &def.view_prop, Value::Float(0.0));
454 }
455 _ => {}
456 }
457 }
458 }
459 return;
460 };
461 for id in 0..ids.len() as u32 {
462 if labels.get(id as usize).copied() != Some(label_sym) {
463 continue;
464 }
465 match compute_view_value(def, id, props, topo, ids, syms, labels) {
466 Some(val) => props.set(id, &def.view_prop, val),
467 None => {
468 props.remove(id, &def.view_prop);
469 }
472 }
473 let _ = et_sym;
477 }
478}
479
480pub fn compute_view_value(
483 def: &ViewDef,
484 node: u32,
485 props: &ColumnStore,
486 topo: &Topology,
487 _ids: &IdMap,
488 syms: &Interner,
489 labels: &[u32],
490) -> Option<Value> {
491 let label_sym = syms.get(&def.label)?;
493 if labels.get(node as usize).copied() != Some(label_sym) {
494 return None;
495 }
496
497 let et_sym = match syms.get(def.edge_type()) {
501 Some(s) => s,
502 None => {
503 return match &def.source {
504 ViewSource::Degree { .. } => Some(Value::Int(0)),
505 ViewSource::NeighborAgg {
506 agg: AggFn::Count, ..
507 } => Some(Value::Int(0)),
508 ViewSource::NeighborAgg {
509 agg: AggFn::Sum, ..
510 } => Some(Value::Float(0.0)),
511 ViewSource::NeighborAgg { .. } => None,
512 };
513 }
514 };
515 let direction = def.direction();
516 let neighbors = topo.neighbors(et_sym, direction, node);
517
518 match &def.source {
519 ViewSource::Degree { .. } => Some(Value::Int(neighbors.len() as i64)),
520 ViewSource::NeighborAgg { agg, prop, .. } => match agg {
521 AggFn::Count => Some(Value::Int(
522 neighbors
523 .iter()
524 .filter(|&&n| props.get(n, prop).is_some())
525 .count() as i64,
526 )),
527 AggFn::Sum => {
528 let mut sum = 0.0f64;
529 for &nbr in neighbors.as_ref() {
530 if let Some(v) = props.get(nbr, prop) {
531 if let Some(n) = as_float(v) {
532 sum += n;
533 }
534 }
535 }
536 Some(Value::Float(sum))
537 }
538 AggFn::Avg => {
539 let mut sum = 0.0f64;
540 let mut count = 0usize;
541 for &nbr in neighbors.as_ref() {
542 if let Some(v) = props.get(nbr, prop) {
543 if let Some(n) = as_float(v) {
544 sum += n;
545 count += 1;
546 }
547 }
548 }
549 if count == 0 {
550 None
551 } else {
552 Some(Value::Float(sum / count as f64))
553 }
554 }
555 AggFn::Min => {
556 let mut best: Option<f64> = None;
557 for &nbr in neighbors.as_ref() {
558 if let Some(v) = props.get(nbr, prop) {
559 if let Some(n) = as_float(v) {
560 best = Some(best.map_or(n, |m: f64| m.min(n)));
561 }
562 }
563 }
564 best.map(Value::Float)
565 }
566 AggFn::Max => {
567 let mut best: Option<f64> = None;
568 for &nbr in neighbors.as_ref() {
569 if let Some(v) = props.get(nbr, prop) {
570 if let Some(n) = as_float(v) {
571 best = Some(best.map_or(n, |m: f64| m.max(n)));
572 }
573 }
574 }
575 best.map(Value::Float)
576 }
577 },
578 }
579}
580
581#[allow(clippy::too_many_arguments)]
587fn update_node_view(
588 def: &ViewDef,
589 subject: u32,
590 neighbor: u32,
591 inserted: bool,
592 props: &mut ColumnStore,
593 topo: &Topology,
594 ids: &IdMap,
595 syms: &Interner,
596 labels: &[u32],
597) {
598 let Some(label_sym) = syms.get(&def.label) else {
600 return;
601 };
602 if labels.get(subject as usize).copied() != Some(label_sym) {
603 return;
604 }
605
606 match &def.source {
607 ViewSource::Degree { .. } => {
608 let current = match props.get(subject, &def.view_prop) {
610 Some(Value::Int(n)) => *n,
611 _ => 0,
612 };
613 let new_val = if inserted {
614 current + 1
615 } else {
616 (current - 1).max(0)
617 };
618 props.set(subject, &def.view_prop, Value::Int(new_val));
619 }
620 ViewSource::NeighborAgg { agg, prop, .. } => {
621 match agg {
622 AggFn::Count => {
623 let has_prop = props.get(neighbor, prop).is_some();
624 let current = match props.get(subject, &def.view_prop) {
625 Some(Value::Int(n)) => *n,
626 _ => 0,
627 };
628 let new_val = if !has_prop {
629 current
630 } else if inserted {
631 current + 1
632 } else {
633 (current - 1).max(0)
634 };
635 props.set(subject, &def.view_prop, Value::Int(new_val));
636 }
637 AggFn::Sum => {
638 let delta = match props.get(neighbor, prop) {
639 Some(v) => as_float(v).unwrap_or(0.0),
640 None => 0.0,
641 };
642 let current = match props.get(subject, &def.view_prop) {
643 Some(Value::Float(f)) => *f,
644 Some(Value::Int(n)) => *n as f64,
645 _ => 0.0,
646 };
647 let new_val = if inserted {
648 current + delta
649 } else {
650 current - delta
651 };
652 props.set(subject, &def.view_prop, Value::Float(new_val));
653 }
654 AggFn::Avg | AggFn::Min | AggFn::Max => {
656 match compute_view_value(def, subject, props, topo, ids, syms, labels) {
657 Some(val) => props.set(subject, &def.view_prop, val),
658 None => {
659 props.remove(subject, &def.view_prop);
660 }
661 }
662 }
663 }
664 }
665 }
666}
667
668fn as_float(v: &Value) -> Option<f64> {
669 match v {
670 Value::Int(i) => Some(*i as f64),
671 Value::Float(f) if f.is_finite() => Some(*f),
672 _ => None,
673 }
674}
675
676#[cfg(test)]
681mod tests {
682 use super::*;
683 use core_storage::{IdMap, Interner, Topology};
684
685 fn make_setup() -> (ViewStore, ColumnStore, Topology, IdMap, Interner, Vec<u32>) {
686 let mut ids = IdMap::new();
687 let mut syms = Interner::new();
688 let mut labels = Vec::new();
689 let mut topo = Topology::new();
690 let mut props = ColumnStore::new();
691
692 let person_sym = syms.intern("Person");
694 let city_sym = syms.intern("City");
695 let edge_sym = syms.intern("LIVES_IN");
696
697 for key in &["p0", "p1", "p2"] {
698 let id = ids.get_or_insert(key);
699 if labels.len() <= id as usize {
700 labels.resize(id as usize + 1, u32::MAX);
701 }
702 labels[id as usize] = person_sym;
703 }
704 let c0 = ids.get_or_insert("c0");
705 if labels.len() <= c0 as usize {
706 labels.resize(c0 as usize + 1, u32::MAX);
707 }
708 labels[c0 as usize] = city_sym;
709
710 let p0 = ids.get("p0").unwrap();
712 let p1 = ids.get("p1").unwrap();
713 topo.add_edge(edge_sym, p0, c0);
714 topo.add_edge(edge_sym, p1, c0);
715
716 props.set(p0, "score", Value::Float(3.0));
718 props.set(p1, "score", Value::Float(7.0));
719
720 (ViewStore::new(), props, topo, ids, syms, labels)
721 }
722
723 #[test]
724 fn degree_view_basic() {
725 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
726 let def = ViewDef {
727 name: "city_pop".into(),
728 label: "City".into(),
729 view_prop: "in_deg".into(),
730 source: ViewSource::Degree {
731 edge_type: "LIVES_IN".into(),
732 direction: Direction::In,
733 },
734 };
735 vs.create_view(def, &mut props, &topo, &ids, &syms, &labels)
736 .unwrap();
737 let c0 = ids.get("c0").unwrap();
738 assert_eq!(props.get(c0, "in_deg"), Some(&Value::Int(2)));
739 }
740
741 #[test]
742 fn neighbor_agg_sum() {
743 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
744 let def = ViewDef {
745 name: "city_score_sum".into(),
746 label: "City".into(),
747 view_prop: "score_sum".into(),
748 source: ViewSource::NeighborAgg {
749 edge_type: "LIVES_IN".into(),
750 direction: Direction::In,
751 agg: AggFn::Sum,
752 prop: "score".into(),
753 },
754 };
755 vs.create_view(def, &mut props, &topo, &ids, &syms, &labels)
756 .unwrap();
757 let c0 = ids.get("c0").unwrap();
758 assert_eq!(props.get(c0, "score_sum"), Some(&Value::Float(10.0)));
760 }
761
762 #[test]
763 fn neighbor_agg_count_skips_missing_prop() {
764 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
765 let p1 = ids.get("p1").unwrap();
766 props.remove(p1, "score");
767 let def = ViewDef {
768 name: "city_score_n".into(),
769 label: "City".into(),
770 view_prop: "score_n".into(),
771 source: ViewSource::NeighborAgg {
772 edge_type: "LIVES_IN".into(),
773 direction: Direction::In,
774 agg: AggFn::Count,
775 prop: "score".into(),
776 },
777 };
778 vs.create_view(def, &mut props, &topo, &ids, &syms, &labels)
779 .unwrap();
780 let c0 = ids.get("c0").unwrap();
781 assert_eq!(props.get(c0, "score_n"), Some(&Value::Int(1)));
782 }
783
784 #[test]
785 fn view_prop_collision_rejected() {
786 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
787 let p0 = ids.get("p0").unwrap();
789 props.set(p0, "collision_prop", Value::Int(1));
790 let def = ViewDef {
791 name: "test_view".into(),
792 label: "Person".into(),
793 view_prop: "collision_prop".into(),
794 source: ViewSource::Degree {
795 edge_type: "LIVES_IN".into(),
796 direction: Direction::Out,
797 },
798 };
799 let err = vs
800 .create_view(def, &mut props, &topo, &ids, &syms, &labels)
801 .unwrap_err();
802 assert!(
803 err.contains("conflicts with an existing node property"),
804 "{err}"
805 );
806 }
807
808 #[test]
809 fn delete_view_removes_values() {
810 let (mut vs, mut props, topo, ids, syms, labels) = make_setup();
811 let def = ViewDef {
812 name: "city_pop".into(),
813 label: "City".into(),
814 view_prop: "in_deg".into(),
815 source: ViewSource::Degree {
816 edge_type: "LIVES_IN".into(),
817 direction: Direction::In,
818 },
819 };
820 vs.create_view(def, &mut props, &topo, &ids, &syms, &labels)
821 .unwrap();
822 let c0 = ids.get("c0").unwrap();
823 assert!(props.get(c0, "in_deg").is_some());
824 vs.delete_view("city_pop", &mut props, &ids, &labels, &syms)
825 .unwrap();
826 assert!(props.get(c0, "in_deg").is_none());
827 }
828}