1use std::collections::HashMap;
4
5use indexmap::IndexMap;
6use parking_lot::Mutex;
7use rayon::prelude::*;
8
9use crate::data::{HCol, HDict, HGrid};
10use crate::filter::{FilterNode, matches_with_ns, parse_filter};
11use crate::kinds::{HRef, Kind};
12use crate::ontology::{DefNamespace, ValidationIssue};
13
14use super::adjacency::RefAdjacency;
15use super::bitmap::TagBitmapIndex;
16use super::changelog::{ChangelogGap, DiffOp, GraphDiff};
17use super::columnar::ColumnarStore;
18use super::csr::CsrAdjacency;
19use super::query_planner;
20use super::value_index::ValueIndex;
21
22#[derive(Debug, thiserror::Error)]
24pub enum GraphError {
25 #[error("entity missing 'id' tag")]
26 MissingId,
27 #[error("entity id must be a Ref")]
28 InvalidId,
29 #[error("entity already exists: {0}")]
30 DuplicateRef(String),
31 #[error("entity not found: {0}")]
32 NotFound(String),
33 #[error("filter error: {0}")]
34 Filter(String),
35}
36
37pub struct EntityGraph {
39 entities: HashMap<String, HDict>,
41 id_map: HashMap<String, usize>,
43 reverse_id: HashMap<usize, String>,
45 next_id: usize,
47 tag_index: TagBitmapIndex,
49 adjacency: RefAdjacency,
51 namespace: Option<DefNamespace>,
53 version: u64,
55 changelog: std::collections::VecDeque<GraphDiff>,
57 changelog_capacity: usize,
59 floor_version: u64,
61 query_cache: Mutex<QueryCache>,
64 ast_cache: Mutex<HashMap<String, FilterNode>>,
66 value_index: ValueIndex,
68 csr: Option<CsrAdjacency>,
71 csr_version: u64,
73 columnar: ColumnarStore,
75}
76
77struct QueryCache {
79 entries: IndexMap<(String, u64), Vec<String>>,
81 capacity: usize,
82}
83
84impl QueryCache {
85 fn new(capacity: usize) -> Self {
86 Self {
87 entries: IndexMap::with_capacity(capacity),
88 capacity,
89 }
90 }
91
92 fn get(&mut self, filter: &str, version: u64) -> Option<&[String]> {
93 let key = (filter.to_string(), version);
95 let idx = self.entries.get_index_of(&key)?;
96 self.entries.move_index(idx, self.entries.len() - 1);
97 self.entries.get(&key).map(|v| v.as_slice())
98 }
99
100 fn insert(&mut self, filter: String, version: u64, ref_vals: Vec<String>) {
101 if self.entries.len() >= self.capacity {
102 self.purge_stale(version);
104 }
105 if self.entries.len() >= self.capacity {
106 self.entries.shift_remove_index(0);
108 }
109 self.entries.insert((filter, version), ref_vals);
110 }
111
112 fn purge_stale(&mut self, min_version: u64) {
114 self.entries
115 .retain(|(_filter, version), _| *version >= min_version);
116 }
117}
118
119const QUERY_CACHE_CAPACITY: usize = 256;
120
121const AUTO_INDEX_FIELDS: &[&str] = &[
123 "siteRef", "equipRef", "dis", "curVal", "area", "geoCity", "kind", "unit",
124];
125
126impl EntityGraph {
127 pub fn new() -> Self {
130 Self::with_changelog_capacity(super::changelog::DEFAULT_CHANGELOG_CAPACITY)
131 }
132
133 pub fn with_changelog_capacity(capacity: usize) -> Self {
135 let capacity = capacity.max(1); let mut value_index = ValueIndex::new();
137 for field in AUTO_INDEX_FIELDS {
138 value_index.index_field(field);
139 }
140 Self {
141 entities: HashMap::new(),
142 id_map: HashMap::new(),
143 reverse_id: HashMap::new(),
144 next_id: 0,
145 tag_index: TagBitmapIndex::new(),
146 adjacency: RefAdjacency::new(),
147 namespace: None,
148 version: 0,
149 changelog: std::collections::VecDeque::new(),
150 changelog_capacity: capacity,
151 floor_version: 0,
152 query_cache: Mutex::new(QueryCache::new(QUERY_CACHE_CAPACITY)),
153 ast_cache: Mutex::new(HashMap::new()),
154 value_index,
155 csr: None,
156 csr_version: 0,
157 columnar: ColumnarStore::new(),
158 }
159 }
160
161 pub fn with_namespace(ns: DefNamespace) -> Self {
163 Self {
164 namespace: Some(ns),
165 ..Self::new()
166 }
167 }
168
169 pub fn index_field(&mut self, field: &str) {
175 self.value_index.index_field(field);
176 }
177
178 pub fn rebuild_value_index(&mut self) {
180 self.value_index.clear();
181 for (ref_val, entity) in &self.entities {
182 if let Some(&eid) = self.id_map.get(ref_val.as_str()) {
183 for (name, val) in entity.iter() {
184 if self.value_index.has_index(name) {
185 self.value_index.add(eid, name, val);
186 }
187 }
188 }
189 }
190 }
191
192 pub fn value_index(&self) -> &ValueIndex {
194 &self.value_index
195 }
196
197 pub fn track_column(&mut self, tag: &str) {
203 self.columnar.track_tag(tag);
204 }
205
206 pub fn rebuild_columnar(&mut self) {
208 self.columnar.clear();
209 self.columnar.ensure_capacity(self.next_id);
210 for (ref_val, entity) in &self.entities {
211 if let Some(&eid) = self.id_map.get(ref_val.as_str()) {
212 for (name, val) in entity.iter() {
213 if self.columnar.is_tracked(name) {
214 self.columnar.set(eid, name, val);
215 }
216 }
217 }
218 }
219 }
220
221 pub fn columnar(&self) -> &ColumnarStore {
223 &self.columnar
224 }
225
226 pub fn add(&mut self, entity: HDict) -> Result<String, GraphError> {
233 let ref_val = extract_ref_val(&entity)?;
234
235 if self.entities.contains_key(&ref_val) {
236 return Err(GraphError::DuplicateRef(ref_val));
237 }
238
239 let eid = self.next_id;
240 self.next_id = self.next_id.checked_add(1).ok_or(GraphError::InvalidId)?;
241
242 self.id_map.insert(ref_val.clone(), eid);
243 self.reverse_id.insert(eid, ref_val.clone());
244
245 self.index_tags(eid, &entity);
247 self.index_refs(eid, &entity);
248
249 let entity_for_log = entity.clone();
251 self.entities.insert(ref_val.clone(), entity);
252
253 self.version += 1;
254 self.csr = None; self.push_changelog(GraphDiff {
256 version: self.version,
257 timestamp: 0,
258 op: DiffOp::Add,
259 ref_val: ref_val.clone(),
260 old: None,
261 new: Some(entity_for_log),
262 changed_tags: None,
263 previous_tags: None,
264 });
265
266 Ok(ref_val)
267 }
268
269 pub fn get(&self, ref_val: &str) -> Option<&HDict> {
271 self.entities.get(ref_val)
272 }
273
274 pub fn update(&mut self, ref_val: &str, changes: HDict) -> Result<(), GraphError> {
279 let eid = *self
280 .id_map
281 .get(ref_val)
282 .ok_or_else(|| GraphError::NotFound(ref_val.to_string()))?;
283
284 let mut old_entity = self
286 .entities
287 .remove(ref_val)
288 .ok_or_else(|| GraphError::NotFound(ref_val.to_string()))?;
289
290 self.remove_indexing(eid, &old_entity);
292
293 let mut prev_tags = HDict::new();
295 let mut changed = HDict::new();
296 for (key, new_val) in changes.iter() {
297 if let Some(old_val) = old_entity.get(key) {
298 prev_tags.set(key, old_val.clone());
299 }
300 changed.set(key, new_val.clone());
301 }
302
303 old_entity.merge(&changes);
305
306 self.index_tags(eid, &old_entity);
308 self.index_refs(eid, &old_entity);
309
310 self.entities.insert(ref_val.to_string(), old_entity);
311
312 self.version += 1;
313 self.csr = None; self.push_changelog(GraphDiff {
315 version: self.version,
316 timestamp: 0,
317 op: DiffOp::Update,
318 ref_val: ref_val.to_string(),
319 old: None,
320 new: None,
321 changed_tags: Some(changed),
322 previous_tags: Some(prev_tags),
323 });
324
325 Ok(())
326 }
327
328 pub fn remove(&mut self, ref_val: &str) -> Result<HDict, GraphError> {
330 let eid = self
331 .id_map
332 .remove(ref_val)
333 .ok_or_else(|| GraphError::NotFound(ref_val.to_string()))?;
334
335 self.reverse_id.remove(&eid);
336
337 let entity = self
338 .entities
339 .remove(ref_val)
340 .ok_or_else(|| GraphError::NotFound(ref_val.to_string()))?;
341
342 self.remove_indexing(eid, &entity);
343
344 self.version += 1;
345 self.csr = None; self.push_changelog(GraphDiff {
347 version: self.version,
348 timestamp: 0,
349 op: DiffOp::Remove,
350 ref_val: ref_val.to_string(),
351 old: Some(entity.clone()),
352 new: None,
353 changed_tags: None,
354 previous_tags: None,
355 });
356
357 Ok(entity)
358 }
359
360 pub fn read(&self, filter_expr: &str, limit: usize) -> Result<HGrid, GraphError> {
364 let results = self.read_all(filter_expr, limit)?;
365
366 if results.is_empty() {
367 return Ok(HGrid::new());
368 }
369
370 let mut seen: std::collections::HashSet<String> =
372 std::collections::HashSet::with_capacity(results.len().min(64));
373 for entity in &results {
374 for name in entity.tag_names() {
375 seen.insert(name.to_string());
376 }
377 }
378 let mut col_set: Vec<String> = seen.into_iter().collect();
379 col_set.sort();
380 let cols: Vec<HCol> = col_set.iter().map(|n| HCol::new(n.as_str())).collect();
381 let rows: Vec<HDict> = results.into_iter().cloned().collect();
382
383 Ok(HGrid::from_parts(HDict::new(), cols, rows))
384 }
385
386 pub fn read_all(&self, filter_expr: &str, limit: usize) -> Result<Vec<&HDict>, GraphError> {
388 let effective_limit = if limit == 0 { usize::MAX } else { limit };
389
390 {
392 let mut cache = self.query_cache.lock();
393 if let Some(cached_refs) = cache.get(filter_expr, self.version) {
394 let mut results = Vec::new();
395 for rv in cached_refs {
396 if results.len() >= effective_limit {
397 break;
398 }
399 if let Some(entity) = self.entities.get(rv) {
400 results.push(entity);
401 }
402 }
403 return Ok(results);
404 }
405 }
406
407 let ast = {
409 let mut ast_cache = self.ast_cache.lock();
410 if let Some(cached) = ast_cache.get(filter_expr) {
411 cached.clone()
412 } else {
413 let parsed =
414 parse_filter(filter_expr).map_err(|e| GraphError::Filter(e.to_string()))?;
415 ast_cache.insert(filter_expr.to_string(), parsed.clone());
416 parsed
417 }
418 };
419
420 let max_id = self.next_id;
422 let candidates = query_planner::bitmap_candidates_with_values(
423 &ast,
424 &self.tag_index,
425 &self.value_index,
426 &self.adjacency,
427 max_id,
428 );
429
430 let resolver = |r: &HRef| -> Option<&HDict> { self.entities.get(&r.val) };
432 let ns = self.namespace.as_ref();
433
434 const PARALLEL_THRESHOLD: usize = 500;
437 let use_parallel = effective_limit == usize::MAX;
438
439 let mut results: Vec<&HDict>;
440
441 if let Some(ref bitmap) = candidates {
442 let candidate_ids: Vec<usize> = TagBitmapIndex::iter_set_bits(bitmap).collect();
443
444 if candidate_ids.len() >= PARALLEL_THRESHOLD && use_parallel {
445 results = candidate_ids
446 .par_iter()
447 .filter_map(|&eid| {
448 let ref_val = self.reverse_id.get(&eid)?;
449 let entity = self.entities.get(ref_val)?;
450 if matches_with_ns(&ast, entity, Some(&resolver), ns) {
451 Some(entity)
452 } else {
453 None
454 }
455 })
456 .collect();
457 } else {
458 results = Vec::new();
459 for eid in TagBitmapIndex::iter_set_bits(bitmap) {
460 if results.len() >= effective_limit {
461 break;
462 }
463 if let Some(ref_val) = self.reverse_id.get(&eid)
464 && let Some(entity) = self.entities.get(ref_val)
465 && matches_with_ns(&ast, entity, Some(&resolver), ns)
466 {
467 results.push(entity);
468 }
469 }
470 }
471 } else {
472 let entity_count = self.entities.len();
473
474 if entity_count >= PARALLEL_THRESHOLD && use_parallel {
475 results = self
476 .entities
477 .par_iter()
478 .filter_map(|(_, entity)| {
479 if matches_with_ns(&ast, entity, Some(&resolver), ns) {
480 Some(entity)
481 } else {
482 None
483 }
484 })
485 .collect();
486 } else {
487 results = Vec::new();
488 for entity in self.entities.values() {
489 if results.len() >= effective_limit {
490 break;
491 }
492 if matches_with_ns(&ast, entity, Some(&resolver), ns) {
493 results.push(entity);
494 }
495 }
496 }
497 }
498
499 if results.len() > effective_limit {
500 results.truncate(effective_limit);
501 }
502
503 if limit == 0 {
506 let ref_vals: Vec<String> = results
507 .iter()
508 .filter_map(|e| {
509 e.get("id").and_then(|k| match k {
510 Kind::Ref(r) => Some(r.val.clone()),
511 _ => None,
512 })
513 })
514 .collect();
515 let mut cache = self.query_cache.lock();
516 cache.insert(filter_expr.to_string(), self.version, ref_vals);
517 }
518
519 Ok(results)
520 }
521
522 pub fn refs_from(&self, ref_val: &str, ref_type: Option<&str>) -> Vec<String> {
526 match self.id_map.get(ref_val) {
527 Some(&eid) => {
528 if let Some(csr) = &self.csr {
529 csr.targets_from(eid, ref_type)
530 } else {
531 self.adjacency.targets_from(eid, ref_type)
532 }
533 }
534 None => Vec::new(),
535 }
536 }
537
538 pub fn refs_to(&self, ref_val: &str, ref_type: Option<&str>) -> Vec<String> {
540 if let Some(csr) = &self.csr {
541 csr.sources_to(ref_val, ref_type)
542 .iter()
543 .filter_map(|eid| self.reverse_id.get(eid).cloned())
544 .collect()
545 } else {
546 self.adjacency
547 .sources_to(ref_val, ref_type)
548 .iter()
549 .filter_map(|eid| self.reverse_id.get(eid).cloned())
550 .collect()
551 }
552 }
553
554 pub fn rebuild_csr(&mut self) {
557 let max_id = if self.next_id > 0 { self.next_id } else { 0 };
558 self.csr = Some(CsrAdjacency::from_ref_adjacency(&self.adjacency, max_id));
559 self.csr_version = self.version;
560 }
561
562 pub fn csr_is_stale(&self) -> bool {
564 match &self.csr {
565 Some(_) => self.csr_version != self.version,
566 None => true,
567 }
568 }
569
570 pub fn all_edges(&self) -> Vec<(String, String, String)> {
574 let mut edges = Vec::new();
575 for (&eid, ref_val) in &self.reverse_id {
576 if let Some(fwd) = self.adjacency.forward_raw().get(&eid) {
577 for (ref_tag, target) in fwd {
578 edges.push((ref_val.clone(), ref_tag.clone(), target.clone()));
579 }
580 }
581 }
582 edges
583 }
584
585 pub fn neighbors(
590 &self,
591 ref_val: &str,
592 hops: usize,
593 ref_types: Option<&[&str]>,
594 ) -> (Vec<&HDict>, Vec<(String, String, String)>) {
595 use std::collections::{HashSet, VecDeque};
596
597 let start_eid = match self.id_map.get(ref_val) {
598 Some(&eid) => eid,
599 None => return (Vec::new(), Vec::new()),
600 };
601
602 let mut visited: HashSet<usize> = HashSet::new();
603 let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
604 let mut result_entities: Vec<&HDict> = Vec::with_capacity(16);
605 let mut result_edges: Vec<(String, String, String)> = Vec::with_capacity(16);
606
607 visited.insert(start_eid);
608 queue.push_back((start_eid, 0));
609
610 if let Some(entity) = self.entities.get(ref_val) {
611 result_entities.push(entity);
612 }
613
614 while let Some((current_eid, depth)) = queue.pop_front() {
615 if depth >= hops {
616 continue;
617 }
618 let current_ref = match self.reverse_id.get(¤t_eid) {
619 Some(s) => s.as_str(),
620 None => continue,
621 };
622
623 if let Some(fwd) = self.adjacency.forward_raw().get(¤t_eid) {
625 for (ref_tag, target) in fwd {
626 if let Some(types) = ref_types
627 && !types.iter().any(|t| t == ref_tag)
628 {
629 continue;
630 }
631 result_edges.push((current_ref.to_string(), ref_tag.clone(), target.clone()));
632 if let Some(&target_eid) = self.id_map.get(target.as_str())
633 && visited.insert(target_eid)
634 {
635 if let Some(entity) = self.entities.get(target.as_str()) {
636 result_entities.push(entity);
637 }
638 queue.push_back((target_eid, depth + 1));
639 }
640 }
641 }
642 if let Some(rev) = self.adjacency.reverse_raw().get(current_ref) {
644 for (ref_tag, source_eid) in rev {
645 if let Some(types) = ref_types
646 && !types.iter().any(|t| t == ref_tag)
647 {
648 continue;
649 }
650 if let Some(source_ref) = self.reverse_id.get(source_eid) {
651 result_edges.push((
652 source_ref.clone(),
653 ref_tag.clone(),
654 current_ref.to_string(),
655 ));
656 if visited.insert(*source_eid) {
657 if let Some(entity) = self.entities.get(source_ref.as_str()) {
658 result_entities.push(entity);
659 }
660 queue.push_back((*source_eid, depth + 1));
661 }
662 }
663 }
664 }
665 }
666
667 result_entities.sort_by(|a, b| {
668 let a_id = a.id().map(|r| r.val.as_str()).unwrap_or("");
669 let b_id = b.id().map(|r| r.val.as_str()).unwrap_or("");
670 a_id.cmp(b_id)
671 });
672 result_edges.sort();
673 result_edges.dedup();
675
676 (result_entities, result_edges)
677 }
678
679 pub fn shortest_path(&self, from: &str, to: &str) -> Vec<String> {
682 use std::collections::{HashMap as StdHashMap, VecDeque};
683
684 if from == to {
685 return vec![from.to_string()];
686 }
687 let from_eid = match self.id_map.get(from) {
688 Some(&eid) => eid,
689 None => return Vec::new(),
690 };
691 let to_eid = match self.id_map.get(to) {
692 Some(&eid) => eid,
693 None => return Vec::new(),
694 };
695
696 let mut parents: StdHashMap<usize, usize> = StdHashMap::new();
698 let mut queue: VecDeque<usize> = VecDeque::new();
699 parents.insert(from_eid, usize::MAX);
700 queue.push_back(from_eid);
701
702 while let Some(current_eid) = queue.pop_front() {
703 let current_ref = match self.reverse_id.get(¤t_eid) {
704 Some(s) => s.as_str(),
705 None => continue,
706 };
707
708 if let Some(fwd) = self.adjacency.forward_raw().get(¤t_eid) {
710 for (_, target) in fwd {
711 if let Some(&target_eid) = self.id_map.get(target.as_str())
712 && let std::collections::hash_map::Entry::Vacant(e) =
713 parents.entry(target_eid)
714 {
715 e.insert(current_eid);
716 if target_eid == to_eid {
717 return Self::reconstruct_path_usize(
718 &parents,
719 to_eid,
720 &self.reverse_id,
721 );
722 }
723 queue.push_back(target_eid);
724 }
725 }
726 }
727 if let Some(rev) = self.adjacency.reverse_raw().get(current_ref) {
729 for (_, source_eid) in rev {
730 if !parents.contains_key(source_eid) {
731 parents.insert(*source_eid, current_eid);
732 if *source_eid == to_eid {
733 return Self::reconstruct_path_usize(
734 &parents,
735 to_eid,
736 &self.reverse_id,
737 );
738 }
739 queue.push_back(*source_eid);
740 }
741 }
742 }
743 }
744
745 Vec::new() }
747
748 fn reconstruct_path_usize(
750 parents: &std::collections::HashMap<usize, usize>,
751 to_eid: usize,
752 reverse_id: &HashMap<usize, String>,
753 ) -> Vec<String> {
754 let mut path_eids = vec![to_eid];
755 let mut current = to_eid;
756 while let Some(&parent) = parents.get(¤t) {
757 if parent == usize::MAX {
758 break;
759 }
760 path_eids.push(parent);
761 current = parent;
762 }
763 path_eids.reverse();
764 path_eids
765 .iter()
766 .filter_map(|eid| reverse_id.get(eid).cloned())
767 .collect()
768 }
769
770 pub fn subtree(&self, root: &str, max_depth: usize) -> Vec<(&HDict, usize)> {
775 use std::collections::{HashSet, VecDeque};
776
777 let mut visited: HashSet<String> = HashSet::new();
778 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
779 let mut results: Vec<(&HDict, usize)> = Vec::new();
780
781 visited.insert(root.to_string());
782 queue.push_back((root.to_string(), 0));
783
784 if let Some(entity) = self.entities.get(root) {
785 results.push((entity, 0));
786 } else {
787 return Vec::new();
788 }
789
790 while let Some((current, depth)) = queue.pop_front() {
791 if depth >= max_depth {
792 continue;
793 }
794 let child_refs = self.refs_to(¤t, None);
796 for child_ref in child_refs {
797 if visited.insert(child_ref.clone())
798 && let Some(entity) = self.entities.get(&child_ref)
799 {
800 results.push((entity, depth + 1));
801 queue.push_back((child_ref, depth + 1));
802 }
803 }
804 }
805
806 results
807 }
808
809 pub fn ref_chain(&self, ref_val: &str, ref_tags: &[&str]) -> Vec<&HDict> {
817 let mut result = Vec::with_capacity(ref_tags.len());
818 let mut current = ref_val.to_string();
819 for tag in ref_tags {
820 let entity = match self.entities.get(¤t) {
821 Some(e) => e,
822 None => break,
823 };
824 match entity.get(tag) {
825 Some(Kind::Ref(r)) => {
826 current = r.val.clone();
827 if let Some(target) = self.entities.get(¤t) {
828 result.push(target);
829 } else {
830 break;
831 }
832 }
833 _ => break,
834 }
835 }
836 result
837 }
838
839 pub fn site_for(&self, ref_val: &str) -> Option<&HDict> {
844 let entity = self.entities.get(ref_val)?;
845 if entity.has("site") {
847 return Some(entity);
848 }
849 if let Some(Kind::Ref(r)) = entity.get("siteRef") {
851 return self.entities.get(&r.val);
852 }
853 if let Some(Kind::Ref(r)) = entity.get("equipRef")
855 && let Some(equip) = self.entities.get(&r.val)
856 && let Some(Kind::Ref(sr)) = equip.get("siteRef")
857 {
858 return self.entities.get(&sr.val);
859 }
860 None
861 }
862
863 pub fn children(&self, ref_val: &str) -> Vec<&HDict> {
865 self.refs_to(ref_val, None)
866 .iter()
867 .filter_map(|r| self.entities.get(r))
868 .collect()
869 }
870
871 pub fn equip_points(&self, equip_ref: &str, filter: Option<&str>) -> Vec<&HDict> {
875 let points: Vec<&HDict> = self
876 .children(equip_ref)
877 .into_iter()
878 .filter(|e| e.has("point"))
879 .collect();
880 match filter {
881 Some(expr) => {
882 let ast = match crate::filter::parse_filter(expr) {
883 Ok(ast) => ast,
884 Err(_) => return points,
885 };
886 points
887 .into_iter()
888 .filter(|e| crate::filter::matches(&ast, e, None))
889 .collect()
890 }
891 None => points,
892 }
893 }
894
895 pub fn entities_fitting(&self, spec_name: &str) -> Vec<&HDict> {
901 match &self.namespace {
902 Some(ns) => self
903 .entities
904 .values()
905 .filter(|e| ns.fits(e, spec_name))
906 .collect(),
907 None => Vec::new(),
908 }
909 }
910
911 pub fn validate(&self) -> Vec<ValidationIssue> {
915 let mut issues: Vec<ValidationIssue> = match &self.namespace {
916 Some(ns) => self
917 .entities
918 .values()
919 .flat_map(|e| ns.validate_entity(e))
920 .collect(),
921 None => Vec::new(),
922 };
923
924 for entity in self.entities.values() {
927 let entity_ref = entity.id().map(|r| r.val.as_str());
928 for (name, val) in entity.iter() {
929 if name == "id" {
930 continue;
931 }
932 if let Kind::Ref(r) = val
933 && !self.entities.contains_key(&r.val)
934 {
935 issues.push(ValidationIssue {
936 entity: entity_ref.map(|s| s.to_string()),
937 issue_type: "dangling_ref".to_string(),
938 detail: format!(
939 "tag '{}' references '{}' which does not exist in the graph",
940 name, r.val
941 ),
942 });
943 }
944 }
945 }
946
947 issues
948 }
949
950 pub fn to_grid(&self, filter_expr: &str) -> Result<HGrid, GraphError> {
957 if filter_expr.is_empty() {
958 let entities: Vec<&HDict> = self.entities.values().collect();
959 return Ok(Self::entities_to_grid(&entities));
960 }
961 self.read(filter_expr, 0)
962 }
963
964 fn entities_to_grid(entities: &[&HDict]) -> HGrid {
966 if entities.is_empty() {
967 return HGrid::new();
968 }
969
970 let mut col_set: Vec<String> = Vec::new();
971 let mut seen = std::collections::HashSet::new();
972 for entity in entities {
973 for name in entity.tag_names() {
974 if seen.insert(name.to_string()) {
975 col_set.push(name.to_string());
976 }
977 }
978 }
979 col_set.sort();
980 let cols: Vec<HCol> = col_set.iter().map(|n| HCol::new(n.as_str())).collect();
981 let rows: Vec<HDict> = entities.iter().map(|e| (*e).clone()).collect();
982
983 HGrid::from_parts(HDict::new(), cols, rows)
984 }
985
986 pub fn from_grid(grid: &HGrid, namespace: Option<DefNamespace>) -> Result<Self, GraphError> {
990 let mut graph = match namespace {
991 Some(ns) => Self::with_namespace(ns),
992 None => Self::new(),
993 };
994 for row in &grid.rows {
995 if row.id().is_some() {
996 graph.add(row.clone())?;
997 }
998 }
999 graph.rebuild_csr();
1001 Ok(graph)
1002 }
1003
1004 pub fn changes_since(&self, version: u64) -> Result<Vec<&GraphDiff>, ChangelogGap> {
1011 let target = version + 1;
1012 if self.floor_version > 0 && version < self.floor_version {
1015 return Err(ChangelogGap {
1016 subscriber_version: version,
1017 floor_version: self.floor_version,
1018 });
1019 }
1020 Ok(self
1022 .changelog
1023 .iter()
1024 .filter(|d| d.version >= target)
1025 .collect())
1026 }
1027
1028 pub fn floor_version(&self) -> u64 {
1032 self.floor_version
1033 }
1034
1035 pub fn changelog_capacity(&self) -> usize {
1037 self.changelog_capacity
1038 }
1039
1040 pub fn version(&self) -> u64 {
1042 self.version
1043 }
1044
1045 pub fn len(&self) -> usize {
1049 self.entities.len()
1050 }
1051
1052 pub fn is_empty(&self) -> bool {
1054 self.entities.is_empty()
1055 }
1056
1057 pub fn contains(&self, ref_val: &str) -> bool {
1059 self.entities.contains_key(ref_val)
1060 }
1061
1062 pub fn all(&self) -> Vec<&HDict> {
1064 self.entities.values().collect()
1065 }
1066
1067 fn index_tags(&mut self, entity_id: usize, entity: &HDict) {
1071 let tags: Vec<String> = entity.tag_names().map(|s| s.to_string()).collect();
1072 self.tag_index.add(entity_id, &tags);
1073
1074 for (name, val) in entity.iter() {
1076 if self.value_index.has_index(name) {
1077 self.value_index.add(entity_id, name, val);
1078 }
1079 if self.columnar.is_tracked(name) {
1081 self.columnar.set(entity_id, name, val);
1082 }
1083 }
1084 }
1085
1086 fn index_refs(&mut self, entity_id: usize, entity: &HDict) {
1088 for (name, val) in entity.iter() {
1089 if let Kind::Ref(r) = val {
1090 if name != "id" {
1093 self.adjacency.add(entity_id, name, &r.val);
1094 }
1095 }
1096 }
1097 }
1098
1099 fn remove_indexing(&mut self, entity_id: usize, entity: &HDict) {
1101 let tags: Vec<String> = entity.tag_names().map(|s| s.to_string()).collect();
1102 self.tag_index.remove(entity_id, &tags);
1103 self.adjacency.remove(entity_id);
1104
1105 for (name, val) in entity.iter() {
1107 if self.value_index.has_index(name) {
1108 self.value_index.remove(entity_id, name, val);
1109 }
1110 }
1111
1112 self.columnar.clear_entity(entity_id);
1114 }
1115
1116 pub fn hierarchy_tree(&self, root: &str, max_depth: usize) -> Option<HierarchyNode> {
1119 let entity = self.entities.get(root)?.clone();
1120 Some(self.build_subtree(root, &entity, 0, max_depth))
1121 }
1122
1123 fn build_subtree(
1124 &self,
1125 ref_val: &str,
1126 entity: &HDict,
1127 depth: usize,
1128 max_depth: usize,
1129 ) -> HierarchyNode {
1130 let children = if depth < max_depth {
1131 self.children(ref_val)
1132 .into_iter()
1133 .filter_map(|child| {
1134 let child_id = child.id()?.val.clone();
1135 Some(self.build_subtree(&child_id, child, depth + 1, max_depth))
1136 })
1137 .collect()
1138 } else {
1139 Vec::new()
1140 };
1141 HierarchyNode {
1142 entity: entity.clone(),
1143 children,
1144 depth,
1145 }
1146 }
1147
1148 pub fn classify(&self, ref_val: &str) -> Option<String> {
1153 let entity = self.entities.get(ref_val)?;
1154 classify_entity(entity)
1155 }
1156
1157 fn push_changelog(&mut self, mut diff: GraphDiff) {
1159 diff.timestamp = GraphDiff::now_nanos();
1160 self.changelog.push_back(diff);
1161 while self.changelog.len() > self.changelog_capacity {
1162 if let Some(evicted) = self.changelog.pop_front() {
1163 self.floor_version = evicted.version;
1164 }
1165 }
1166 }
1167}
1168
1169impl Default for EntityGraph {
1170 fn default() -> Self {
1171 Self::new()
1172 }
1173}
1174
1175#[derive(Debug, Clone)]
1177pub struct HierarchyNode {
1178 pub entity: HDict,
1179 pub children: Vec<HierarchyNode>,
1180 pub depth: usize,
1181}
1182
1183fn extract_ref_val(entity: &HDict) -> Result<String, GraphError> {
1185 match entity.get("id") {
1186 Some(Kind::Ref(r)) => Ok(r.val.clone()),
1187 Some(_) => Err(GraphError::InvalidId),
1188 None => Err(GraphError::MissingId),
1189 }
1190}
1191
1192const CLASSIFY_PRIORITY: &[&str] = &[
1195 "sensor", "cmd", "sp", "ahu", "vav", "boiler", "chiller", "meter", "point", "equip", "room", "floor", "zone", "space", "site", "weather", "device", "network",
1202];
1203
1204fn classify_entity(entity: &HDict) -> Option<String> {
1206 for &tag in CLASSIFY_PRIORITY {
1207 if entity.has(tag) {
1208 return Some(tag.to_string());
1209 }
1210 }
1211 None
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217 use crate::kinds::Number;
1218
1219 fn make_site(id: &str) -> HDict {
1220 let mut d = HDict::new();
1221 d.set("id", Kind::Ref(HRef::from_val(id)));
1222 d.set("site", Kind::Marker);
1223 d.set("dis", Kind::Str(format!("Site {id}")));
1224 d.set(
1225 "area",
1226 Kind::Number(Number::new(4500.0, Some("ft\u{00b2}".into()))),
1227 );
1228 d
1229 }
1230
1231 fn make_equip(id: &str, site_ref: &str) -> HDict {
1232 let mut d = HDict::new();
1233 d.set("id", Kind::Ref(HRef::from_val(id)));
1234 d.set("equip", Kind::Marker);
1235 d.set("dis", Kind::Str(format!("Equip {id}")));
1236 d.set("siteRef", Kind::Ref(HRef::from_val(site_ref)));
1237 d
1238 }
1239
1240 fn make_point(id: &str, equip_ref: &str) -> HDict {
1241 let mut d = HDict::new();
1242 d.set("id", Kind::Ref(HRef::from_val(id)));
1243 d.set("point", Kind::Marker);
1244 d.set("sensor", Kind::Marker);
1245 d.set("temp", Kind::Marker);
1246 d.set("dis", Kind::Str(format!("Point {id}")));
1247 d.set("equipRef", Kind::Ref(HRef::from_val(equip_ref)));
1248 d.set(
1249 "curVal",
1250 Kind::Number(Number::new(72.5, Some("\u{00b0}F".into()))),
1251 );
1252 d
1253 }
1254
1255 #[test]
1258 fn add_entity_with_valid_id() {
1259 let mut g = EntityGraph::new();
1260 let result = g.add(make_site("site-1"));
1261 assert!(result.is_ok());
1262 assert_eq!(result.unwrap(), "site-1");
1263 assert_eq!(g.len(), 1);
1264 }
1265
1266 #[test]
1267 fn add_entity_missing_id_fails() {
1268 let mut g = EntityGraph::new();
1269 let entity = HDict::new();
1270 let err = g.add(entity).unwrap_err();
1271 assert!(matches!(err, GraphError::MissingId));
1272 }
1273
1274 #[test]
1275 fn add_entity_non_ref_id_fails() {
1276 let mut g = EntityGraph::new();
1277 let mut entity = HDict::new();
1278 entity.set("id", Kind::Str("not-a-ref".into()));
1279 let err = g.add(entity).unwrap_err();
1280 assert!(matches!(err, GraphError::InvalidId));
1281 }
1282
1283 #[test]
1284 fn add_duplicate_ref_fails() {
1285 let mut g = EntityGraph::new();
1286 g.add(make_site("site-1")).unwrap();
1287 let err = g.add(make_site("site-1")).unwrap_err();
1288 assert!(matches!(err, GraphError::DuplicateRef(_)));
1289 }
1290
1291 #[test]
1294 fn get_existing_entity() {
1295 let mut g = EntityGraph::new();
1296 g.add(make_site("site-1")).unwrap();
1297 let entity = g.get("site-1").unwrap();
1298 assert!(entity.has("site"));
1299 assert_eq!(entity.get("dis"), Some(&Kind::Str("Site site-1".into())));
1300 }
1301
1302 #[test]
1303 fn get_missing_entity_returns_none() {
1304 let g = EntityGraph::new();
1305 assert!(g.get("nonexistent").is_none());
1306 }
1307
1308 #[test]
1311 fn update_merges_changes() {
1312 let mut g = EntityGraph::new();
1313 g.add(make_site("site-1")).unwrap();
1314
1315 let mut changes = HDict::new();
1316 changes.set("dis", Kind::Str("Updated Site".into()));
1317 changes.set("geoCity", Kind::Str("Richmond".into()));
1318 g.update("site-1", changes).unwrap();
1319
1320 let entity = g.get("site-1").unwrap();
1321 assert_eq!(entity.get("dis"), Some(&Kind::Str("Updated Site".into())));
1322 assert_eq!(entity.get("geoCity"), Some(&Kind::Str("Richmond".into())));
1323 assert!(entity.has("site")); }
1325
1326 #[test]
1327 fn update_missing_entity_fails() {
1328 let mut g = EntityGraph::new();
1329 let err = g.update("nonexistent", HDict::new()).unwrap_err();
1330 assert!(matches!(err, GraphError::NotFound(_)));
1331 }
1332
1333 #[test]
1336 fn remove_entity() {
1337 let mut g = EntityGraph::new();
1338 g.add(make_site("site-1")).unwrap();
1339 let removed = g.remove("site-1").unwrap();
1340 assert!(removed.has("site"));
1341 assert!(g.get("site-1").is_none());
1342 assert_eq!(g.len(), 0);
1343 }
1344
1345 #[test]
1346 fn remove_missing_entity_fails() {
1347 let mut g = EntityGraph::new();
1348 let err = g.remove("nonexistent").unwrap_err();
1349 assert!(matches!(err, GraphError::NotFound(_)));
1350 }
1351
1352 #[test]
1355 fn version_increments_on_mutations() {
1356 let mut g = EntityGraph::new();
1357 assert_eq!(g.version(), 0);
1358
1359 g.add(make_site("site-1")).unwrap();
1360 assert_eq!(g.version(), 1);
1361
1362 g.update("site-1", HDict::new()).unwrap();
1363 assert_eq!(g.version(), 2);
1364
1365 g.remove("site-1").unwrap();
1366 assert_eq!(g.version(), 3);
1367 }
1368
1369 #[test]
1370 fn changelog_records_add_update_remove() {
1371 let mut g = EntityGraph::new();
1372 g.add(make_site("site-1")).unwrap();
1373 g.update("site-1", HDict::new()).unwrap();
1374 g.remove("site-1").unwrap();
1375
1376 let changes = g.changes_since(0).unwrap();
1377 assert_eq!(changes.len(), 3);
1378
1379 assert_eq!(changes[0].op, DiffOp::Add);
1381 assert_eq!(changes[0].ref_val, "site-1");
1382 assert!(changes[0].old.is_none());
1383 assert!(changes[0].new.is_some());
1384 assert!(changes[0].changed_tags.is_none());
1385
1386 assert_eq!(changes[1].op, DiffOp::Update);
1388 assert!(changes[1].old.is_none());
1389 assert!(changes[1].new.is_none());
1390 assert!(changes[1].changed_tags.is_some());
1391 assert!(changes[1].previous_tags.is_some());
1392
1393 assert_eq!(changes[2].op, DiffOp::Remove);
1395 assert!(changes[2].old.is_some());
1396 assert!(changes[2].new.is_none());
1397 assert!(changes[2].changed_tags.is_none());
1398 }
1399
1400 #[test]
1401 fn changes_since_returns_subset() {
1402 let mut g = EntityGraph::new();
1403 g.add(make_site("site-1")).unwrap(); g.add(make_site("site-2")).unwrap(); g.add(make_site("site-3")).unwrap(); let since_v2 = g.changes_since(2).unwrap();
1408 assert_eq!(since_v2.len(), 1);
1409 assert_eq!(since_v2[0].ref_val, "site-3");
1410 }
1411
1412 #[test]
1413 fn configurable_changelog_capacity() {
1414 let mut g = EntityGraph::with_changelog_capacity(3);
1415 assert_eq!(g.changelog_capacity(), 3);
1416
1417 for i in 0..5 {
1419 g.add(make_site(&format!("site-{i}"))).unwrap();
1420 }
1421
1422 assert_eq!(g.version(), 5);
1423 assert_eq!(g.floor_version(), 2); let changes = g.changes_since(2).unwrap();
1427 assert_eq!(changes.len(), 3); let gap = g.changes_since(1).unwrap_err();
1431 assert_eq!(gap.subscriber_version, 1);
1432 assert_eq!(gap.floor_version, 2);
1433 }
1434
1435 #[test]
1436 fn changelog_gap_on_version_zero_after_eviction() {
1437 let mut g = EntityGraph::with_changelog_capacity(2);
1438 for i in 0..4 {
1439 g.add(make_site(&format!("site-{i}"))).unwrap();
1440 }
1441
1442 let gap = g.changes_since(0).unwrap_err();
1444 assert_eq!(gap.subscriber_version, 0);
1445 assert!(gap.floor_version > 0);
1446 }
1447
1448 #[test]
1449 fn no_gap_when_capacity_sufficient() {
1450 let mut g = EntityGraph::with_changelog_capacity(100);
1451 for i in 0..50 {
1452 g.add(make_site(&format!("site-{i}"))).unwrap();
1453 }
1454 assert_eq!(g.floor_version(), 0);
1455 let changes = g.changes_since(0).unwrap();
1456 assert_eq!(changes.len(), 50);
1457 }
1458
1459 #[test]
1460 fn changelog_entries_have_timestamps() {
1461 let mut g = EntityGraph::new();
1462 g.add(make_site("site-1")).unwrap();
1463 g.update("site-1", HDict::new()).unwrap();
1464 g.remove("site-1").unwrap();
1465
1466 let changes = g.changes_since(0).unwrap();
1467 for diff in &changes {
1468 assert!(diff.timestamp > 0, "timestamp should be positive");
1469 }
1470 for pair in changes.windows(2) {
1472 assert!(pair[1].timestamp >= pair[0].timestamp);
1473 }
1474 }
1475
1476 #[test]
1477 fn update_diff_carries_delta_tags() {
1478 let mut g = EntityGraph::new();
1479 let mut site = HDict::new();
1480 site.set("id", Kind::Ref(HRef::from_val("site-1")));
1481 site.set("site", Kind::Marker);
1482 site.set("dis", Kind::Str("Original".into()));
1483 site.set("area", Kind::Number(Number::unitless(1000.0)));
1484 g.add(site).unwrap();
1485
1486 let mut changes = HDict::new();
1487 changes.set("dis", Kind::Str("Updated".into()));
1488 g.update("site-1", changes).unwrap();
1489
1490 let diffs = g.changes_since(1).unwrap(); assert_eq!(diffs.len(), 1);
1492 let diff = &diffs[0];
1493 assert_eq!(diff.op, DiffOp::Update);
1494
1495 assert!(diff.old.is_none());
1497 assert!(diff.new.is_none());
1498
1499 let ct = diff.changed_tags.as_ref().unwrap();
1501 assert_eq!(ct.get("dis"), Some(&Kind::Str("Updated".into())));
1502 assert!(ct.get("area").is_none()); let pt = diff.previous_tags.as_ref().unwrap();
1506 assert_eq!(pt.get("dis"), Some(&Kind::Str("Original".into())));
1507 }
1508
1509 #[test]
1512 fn contains_check() {
1513 let mut g = EntityGraph::new();
1514 g.add(make_site("site-1")).unwrap();
1515 assert!(g.contains("site-1"));
1516 assert!(!g.contains("site-2"));
1517 }
1518
1519 #[test]
1520 fn len_and_is_empty() {
1521 let mut g = EntityGraph::new();
1522 assert!(g.is_empty());
1523 assert_eq!(g.len(), 0);
1524
1525 g.add(make_site("site-1")).unwrap();
1526 assert!(!g.is_empty());
1527 assert_eq!(g.len(), 1);
1528 }
1529
1530 #[test]
1533 fn read_with_simple_has_filter() {
1534 let mut g = EntityGraph::new();
1535 g.add(make_site("site-1")).unwrap();
1536 g.add(make_equip("equip-1", "site-1")).unwrap();
1537
1538 let results = g.read_all("site", 0).unwrap();
1539 assert_eq!(results.len(), 1);
1540 assert!(results[0].has("site"));
1541 }
1542
1543 #[test]
1544 fn read_with_comparison_filter() {
1545 let mut g = EntityGraph::new();
1546 g.add(make_point("pt-1", "equip-1")).unwrap();
1547
1548 let results = g.read_all("curVal > 70\u{00b0}F", 0).unwrap();
1549 assert_eq!(results.len(), 1);
1550 }
1551
1552 #[test]
1553 fn read_with_and_filter() {
1554 let mut g = EntityGraph::new();
1555 g.add(make_point("pt-1", "equip-1")).unwrap();
1556 g.add(make_equip("equip-1", "site-1")).unwrap();
1557
1558 let results = g.read_all("point and sensor", 0).unwrap();
1559 assert_eq!(results.len(), 1);
1560 }
1561
1562 #[test]
1563 fn read_with_or_filter() {
1564 let mut g = EntityGraph::new();
1565 g.add(make_site("site-1")).unwrap();
1566 g.add(make_equip("equip-1", "site-1")).unwrap();
1567
1568 let results = g.read_all("site or equip", 0).unwrap();
1569 assert_eq!(results.len(), 2);
1570 }
1571
1572 #[test]
1573 fn read_limit_parameter_works() {
1574 let mut g = EntityGraph::new();
1575 g.add(make_site("site-1")).unwrap();
1576 g.add(make_site("site-2")).unwrap();
1577 g.add(make_site("site-3")).unwrap();
1578
1579 let results = g.read_all("site", 2).unwrap();
1580 assert_eq!(results.len(), 2);
1581 }
1582
1583 #[test]
1584 fn read_returns_grid() {
1585 let mut g = EntityGraph::new();
1586 g.add(make_site("site-1")).unwrap();
1587 g.add(make_site("site-2")).unwrap();
1588
1589 let grid = g.read("site", 0).unwrap();
1590 assert_eq!(grid.len(), 2);
1591 assert!(grid.col("site").is_some());
1592 assert!(grid.col("id").is_some());
1593 }
1594
1595 #[test]
1596 fn read_invalid_filter() {
1597 let g = EntityGraph::new();
1598 let err = g.read("!!!", 0).unwrap_err();
1599 assert!(matches!(err, GraphError::Filter(_)));
1600 }
1601
1602 #[test]
1603 fn query_cache_returns_same_results() {
1604 let mut g = EntityGraph::new();
1605 g.add(make_site("site-1")).unwrap();
1606 g.add(make_equip("equip-1", "site-1")).unwrap();
1607 g.add(make_point("pt-1", "equip-1")).unwrap();
1608
1609 let results1 = g.read_all("site", 0).unwrap();
1611 assert_eq!(results1.len(), 1);
1612
1613 let results2 = g.read_all("site", 0).unwrap();
1615 assert_eq!(results2.len(), 1);
1616 assert_eq!(results1[0].get("id"), results2[0].get("id"));
1617 }
1618
1619 #[test]
1620 fn query_cache_invalidated_by_mutation() {
1621 let mut g = EntityGraph::new();
1622 g.add(make_site("site-1")).unwrap();
1623
1624 let results = g.read_all("site", 0).unwrap();
1625 assert_eq!(results.len(), 1);
1626
1627 g.add(make_site("site-2")).unwrap();
1629
1630 let results = g.read_all("site", 0).unwrap();
1631 assert_eq!(results.len(), 2);
1632 }
1633
1634 #[test]
1637 fn refs_from_returns_targets() {
1638 let mut g = EntityGraph::new();
1639 g.add(make_site("site-1")).unwrap();
1640 g.add(make_equip("equip-1", "site-1")).unwrap();
1641
1642 let targets = g.refs_from("equip-1", None);
1643 assert_eq!(targets, vec!["site-1".to_string()]);
1644 }
1645
1646 #[test]
1647 fn refs_to_returns_sources() {
1648 let mut g = EntityGraph::new();
1649 g.add(make_site("site-1")).unwrap();
1650 g.add(make_equip("equip-1", "site-1")).unwrap();
1651 g.add(make_equip("equip-2", "site-1")).unwrap();
1652
1653 let mut sources = g.refs_to("site-1", None);
1654 sources.sort();
1655 assert_eq!(sources.len(), 2);
1656 }
1657
1658 #[test]
1659 fn type_filtered_ref_queries() {
1660 let mut g = EntityGraph::new();
1661 g.add(make_site("site-1")).unwrap();
1662 g.add(make_equip("equip-1", "site-1")).unwrap();
1663
1664 let targets = g.refs_from("equip-1", Some("siteRef"));
1665 assert_eq!(targets, vec!["site-1".to_string()]);
1666
1667 let targets = g.refs_from("equip-1", Some("equipRef"));
1668 assert!(targets.is_empty());
1669 }
1670
1671 #[test]
1672 fn refs_from_nonexistent_entity() {
1673 let g = EntityGraph::new();
1674 assert!(g.refs_from("nonexistent", None).is_empty());
1675 }
1676
1677 #[test]
1678 fn refs_to_nonexistent_entity() {
1679 let g = EntityGraph::new();
1680 assert!(g.refs_to("nonexistent", None).is_empty());
1681 }
1682
1683 #[test]
1686 fn from_grid_round_trip() {
1687 let mut g = EntityGraph::new();
1688 g.add(make_site("site-1")).unwrap();
1689 g.add(make_equip("equip-1", "site-1")).unwrap();
1690
1691 let grid = g.to_grid("site or equip").unwrap();
1692 assert_eq!(grid.len(), 2);
1693
1694 let g2 = EntityGraph::from_grid(&grid, None).unwrap();
1695 assert_eq!(g2.len(), 2);
1696 assert!(g2.contains("site-1"));
1697 assert!(g2.contains("equip-1"));
1698 }
1699
1700 #[test]
1701 fn to_grid_empty_result() {
1702 let g = EntityGraph::new();
1703 let grid = g.to_grid("site").unwrap();
1704 assert!(grid.is_empty());
1705 }
1706
1707 #[test]
1710 fn update_reindexes_tags() {
1711 let mut g = EntityGraph::new();
1712 g.add(make_site("site-1")).unwrap();
1713
1714 assert_eq!(g.read_all("site", 0).unwrap().len(), 1);
1716
1717 let mut changes = HDict::new();
1719 changes.set("site", Kind::Remove);
1720 g.update("site-1", changes).unwrap();
1721
1722 assert_eq!(g.read_all("site", 0).unwrap().len(), 0);
1724 }
1725
1726 #[test]
1727 fn update_reindexes_refs() {
1728 let mut g = EntityGraph::new();
1729 g.add(make_site("site-1")).unwrap();
1730 g.add(make_site("site-2")).unwrap();
1731 g.add(make_equip("equip-1", "site-1")).unwrap();
1732
1733 assert_eq!(g.refs_from("equip-1", None), vec!["site-1".to_string()]);
1735
1736 let mut changes = HDict::new();
1738 changes.set("siteRef", Kind::Ref(HRef::from_val("site-2")));
1739 g.update("equip-1", changes).unwrap();
1740
1741 assert_eq!(g.refs_from("equip-1", None), vec!["site-2".to_string()]);
1742 assert!(g.refs_to("site-1", None).is_empty());
1743 }
1744
1745 #[test]
1748 fn validate_detects_dangling_refs() {
1749 let mut g = EntityGraph::new();
1750 g.add(make_site("site-1")).unwrap();
1751 g.add(make_equip("equip-1", "site-1")).unwrap();
1753 g.add(make_equip("equip-2", "site-999")).unwrap();
1755
1756 let issues = g.validate();
1757 assert!(!issues.is_empty());
1758
1759 let dangling: Vec<_> = issues
1760 .iter()
1761 .filter(|i| i.issue_type == "dangling_ref")
1762 .collect();
1763 assert_eq!(dangling.len(), 1);
1764 assert_eq!(dangling[0].entity.as_deref(), Some("equip-2"));
1765 assert!(dangling[0].detail.contains("site-999"));
1766 assert!(dangling[0].detail.contains("siteRef"));
1767 }
1768
1769 #[test]
1772 fn to_grid_empty_filter_exports_all() {
1773 let mut g = EntityGraph::new();
1774 g.add(make_site("site-1")).unwrap();
1775 g.add(make_equip("equip-1", "site-1")).unwrap();
1776 g.add(make_point("pt-1", "equip-1")).unwrap();
1777
1778 let grid = g.to_grid("").unwrap();
1779 assert_eq!(grid.len(), 3);
1780 assert!(grid.col("id").is_some());
1781 }
1782
1783 #[test]
1786 fn changelog_bounded_to_max_size() {
1787 let mut graph = EntityGraph::with_changelog_capacity(100);
1789 for i in 0..200 {
1790 let mut d = HDict::new();
1791 d.set("id", Kind::Ref(HRef::from_val(format!("e{i}"))));
1792 d.set("dis", Kind::Str(format!("Entity {i}")));
1793 graph.add(d).unwrap();
1794 }
1795 let floor = graph.floor_version();
1798 assert!(floor > 0);
1799 let changes = graph.changes_since(floor).unwrap();
1800 assert!(changes.len() <= 100);
1801 assert!(graph.changes_since(199).unwrap().len() <= 1);
1803 assert!(graph.changes_since(0).is_err());
1805 }
1806
1807 #[test]
1808 fn from_grid_skips_rows_without_id() {
1809 let cols = vec![HCol::new("id"), HCol::new("dis"), HCol::new("site")];
1810
1811 let mut row_with_id = HDict::new();
1812 row_with_id.set("id", Kind::Ref(HRef::from_val("site-1")));
1813 row_with_id.set("site", Kind::Marker);
1814 row_with_id.set("dis", Kind::Str("Has ID".into()));
1815
1816 let mut row_bad_id = HDict::new();
1818 row_bad_id.set("id", Kind::Str("not-a-ref".into()));
1819 row_bad_id.set("dis", Kind::Str("Bad ID".into()));
1820
1821 let mut row_no_id = HDict::new();
1823 row_no_id.set("dis", Kind::Str("No ID".into()));
1824
1825 let grid = HGrid::from_parts(HDict::new(), cols, vec![row_with_id, row_bad_id, row_no_id]);
1826 let g = EntityGraph::from_grid(&grid, None).unwrap();
1827
1828 assert_eq!(g.len(), 1);
1829 assert!(g.contains("site-1"));
1830 }
1831
1832 fn build_hierarchy_graph() -> EntityGraph {
1835 let mut g = EntityGraph::new();
1836 g.add(make_site("s1")).unwrap();
1837 g.add(make_site("s2")).unwrap();
1838 g.add(make_equip("e1", "s1")).unwrap();
1839 g.add(make_equip("e2", "s1")).unwrap();
1840 g.add(make_equip("e3", "s2")).unwrap();
1841 g.add(make_point("p1", "e1")).unwrap();
1842 g.add(make_point("p2", "e1")).unwrap();
1843 g.add(make_point("p3", "e2")).unwrap();
1844 g
1845 }
1846
1847 #[test]
1848 fn all_edges_returns_all_ref_relationships() {
1849 let g = build_hierarchy_graph();
1850 let edges = g.all_edges();
1851 assert_eq!(edges.len(), 6);
1853 assert!(
1854 edges
1855 .iter()
1856 .any(|(s, t, d)| s == "e1" && t == "siteRef" && d == "s1")
1857 );
1858 assert!(
1859 edges
1860 .iter()
1861 .any(|(s, t, d)| s == "p1" && t == "equipRef" && d == "e1")
1862 );
1863 }
1864
1865 #[test]
1866 fn all_edges_empty_graph() {
1867 let g = EntityGraph::new();
1868 assert!(g.all_edges().is_empty());
1869 }
1870
1871 #[test]
1872 fn neighbors_one_hop() {
1873 let g = build_hierarchy_graph();
1874 let (entities, edges) = g.neighbors("e1", 1, None);
1875 let ids: Vec<String> = entities
1877 .iter()
1878 .filter_map(|e| e.id().map(|r| r.val.clone()))
1879 .collect();
1880 assert!(ids.contains(&"e1".to_string()));
1881 assert!(ids.contains(&"s1".to_string()));
1882 assert!(ids.contains(&"p1".to_string()));
1883 assert!(ids.contains(&"p2".to_string()));
1884 assert!(!edges.is_empty());
1885 }
1886
1887 #[test]
1888 fn neighbors_with_ref_type_filter() {
1889 let g = build_hierarchy_graph();
1890 let (entities, edges) = g.neighbors("e1", 1, Some(&["siteRef"]));
1891 let ids: Vec<String> = entities
1893 .iter()
1894 .filter_map(|e| e.id().map(|r| r.val.clone()))
1895 .collect();
1896 assert!(ids.contains(&"e1".to_string()));
1897 assert!(ids.contains(&"s1".to_string()));
1898 assert!(!ids.contains(&"p1".to_string()));
1900 assert!(edges.iter().all(|(_, tag, _)| tag == "siteRef"));
1902 }
1903
1904 #[test]
1905 fn neighbors_zero_hops() {
1906 let g = build_hierarchy_graph();
1907 let (entities, edges) = g.neighbors("e1", 0, None);
1908 assert_eq!(entities.len(), 1);
1909 assert!(edges.is_empty());
1910 }
1911
1912 #[test]
1913 fn neighbors_nonexistent_entity() {
1914 let g = build_hierarchy_graph();
1915 let (entities, _) = g.neighbors("nonexistent", 1, None);
1916 assert!(entities.is_empty());
1917 }
1918
1919 #[test]
1920 fn shortest_path_direct() {
1921 let g = build_hierarchy_graph();
1922 let path = g.shortest_path("e1", "s1");
1923 assert_eq!(path, vec!["e1".to_string(), "s1".to_string()]);
1924 }
1925
1926 #[test]
1927 fn shortest_path_two_hops() {
1928 let g = build_hierarchy_graph();
1929 let path = g.shortest_path("p1", "s1");
1930 assert_eq!(path.len(), 3);
1932 assert_eq!(path[0], "p1");
1933 assert_eq!(path[2], "s1");
1934 }
1935
1936 #[test]
1937 fn shortest_path_same_node() {
1938 let g = build_hierarchy_graph();
1939 let path = g.shortest_path("s1", "s1");
1940 assert_eq!(path, vec!["s1".to_string()]);
1941 }
1942
1943 #[test]
1944 fn shortest_path_no_connection() {
1945 let g = build_hierarchy_graph();
1947 let path = g.shortest_path("s1", "s2");
1948 assert!(path.is_empty());
1950 }
1951
1952 #[test]
1953 fn shortest_path_nonexistent() {
1954 let g = build_hierarchy_graph();
1955 let path = g.shortest_path("s1", "nonexistent");
1956 assert!(path.is_empty());
1957 }
1958
1959 #[test]
1960 fn subtree_from_site() {
1961 let g = build_hierarchy_graph();
1962 let tree = g.subtree("s1", 10);
1963 assert_eq!(tree.len(), 6);
1965 assert_eq!(tree[0].0.id().unwrap().val, "s1");
1967 assert_eq!(tree[0].1, 0);
1968 let depth_1: Vec<_> = tree.iter().filter(|(_, d)| *d == 1).collect();
1970 assert_eq!(depth_1.len(), 2);
1971 let depth_2: Vec<_> = tree.iter().filter(|(_, d)| *d == 2).collect();
1973 assert_eq!(depth_2.len(), 3);
1974 }
1975
1976 #[test]
1977 fn subtree_max_depth_1() {
1978 let g = build_hierarchy_graph();
1979 let tree = g.subtree("s1", 1);
1980 assert_eq!(tree.len(), 3);
1982 }
1983
1984 #[test]
1985 fn subtree_nonexistent_root() {
1986 let g = build_hierarchy_graph();
1987 let tree = g.subtree("nonexistent", 10);
1988 assert!(tree.is_empty());
1989 }
1990
1991 #[test]
1992 fn subtree_leaf_node() {
1993 let g = build_hierarchy_graph();
1994 let tree = g.subtree("p1", 10);
1995 assert_eq!(tree.len(), 1);
1997 assert_eq!(tree[0].0.id().unwrap().val, "p1");
1998 }
1999
2000 #[test]
2003 fn ref_chain_walks_equip_to_site() {
2004 let g = build_hierarchy_graph();
2005 let chain = g.ref_chain("p1", &["equipRef", "siteRef"]);
2007 assert_eq!(chain.len(), 2);
2008 assert_eq!(chain[0].id().unwrap().val, "e1");
2009 assert_eq!(chain[1].id().unwrap().val, "s1");
2010 }
2011
2012 #[test]
2013 fn ref_chain_stops_on_missing_tag() {
2014 let g = build_hierarchy_graph();
2015 let chain = g.ref_chain("e1", &["siteRef", "spaceRef"]);
2017 assert_eq!(chain.len(), 1);
2018 assert_eq!(chain[0].id().unwrap().val, "s1");
2019 }
2020
2021 #[test]
2022 fn ref_chain_empty_for_nonexistent() {
2023 let g = build_hierarchy_graph();
2024 let chain = g.ref_chain("nonexistent", &["equipRef"]);
2025 assert!(chain.is_empty());
2026 }
2027
2028 #[test]
2029 fn site_for_returns_site_itself() {
2030 let g = build_hierarchy_graph();
2031 let site = g.site_for("s1").unwrap();
2032 assert_eq!(site.id().unwrap().val, "s1");
2033 }
2034
2035 #[test]
2036 fn site_for_walks_from_point() {
2037 let g = build_hierarchy_graph();
2038 let site = g.site_for("p1").unwrap();
2040 assert_eq!(site.id().unwrap().val, "s1");
2041 }
2042
2043 #[test]
2044 fn site_for_walks_from_equip() {
2045 let g = build_hierarchy_graph();
2046 let site = g.site_for("e1").unwrap();
2047 assert_eq!(site.id().unwrap().val, "s1");
2048 }
2049
2050 #[test]
2051 fn children_returns_direct_refs() {
2052 let g = build_hierarchy_graph();
2053 let kids = g.children("s1");
2054 let ids: Vec<&str> = kids.iter().map(|e| e.id().unwrap().val.as_str()).collect();
2056 assert!(ids.contains(&"e1"));
2057 assert!(ids.contains(&"e2"));
2058 }
2059
2060 #[test]
2061 fn equip_points_returns_points_only() {
2062 let g = build_hierarchy_graph();
2063 let points = g.equip_points("e1", None);
2064 assert_eq!(points.len(), 2); for p in &points {
2066 assert!(p.has("point"));
2067 }
2068 }
2069
2070 #[test]
2071 fn equip_points_with_filter() {
2072 let mut g = build_hierarchy_graph();
2073 let mut pf = HDict::new();
2075 pf.set("id", Kind::Ref(HRef::from_val("pf")));
2076 pf.set("point", Kind::Marker);
2077 pf.set("flow", Kind::Marker);
2078 pf.set("equipRef", Kind::Ref(HRef::from_val("e1")));
2079 g.add(pf).unwrap();
2080
2081 let temp_points = g.equip_points("e1", Some("temp"));
2082 assert_eq!(temp_points.len(), 2);
2084 assert!(temp_points.iter().all(|p| p.has("temp")));
2085 }
2086
2087 #[test]
2090 fn hierarchy_tree_from_site() {
2091 let g = build_hierarchy_graph();
2092 let tree = g.hierarchy_tree("s1", 10).unwrap();
2093 assert_eq!(tree.depth, 0);
2094 assert_eq!(tree.entity.id().unwrap().val, "s1");
2095 assert_eq!(tree.children.len(), 2);
2097 let child_ids: Vec<String> = tree
2098 .children
2099 .iter()
2100 .map(|c| c.entity.id().unwrap().val.clone())
2101 .collect();
2102 assert!(child_ids.contains(&"e1".to_string()));
2103 assert!(child_ids.contains(&"e2".to_string()));
2104 let e1_node = tree
2106 .children
2107 .iter()
2108 .find(|c| c.entity.id().unwrap().val == "e1")
2109 .unwrap();
2110 assert_eq!(e1_node.children.len(), 2);
2111 let point_ids: Vec<String> = e1_node
2112 .children
2113 .iter()
2114 .map(|c| c.entity.id().unwrap().val.clone())
2115 .collect();
2116 assert!(point_ids.contains(&"p1".to_string()));
2117 assert!(point_ids.contains(&"p2".to_string()));
2118 }
2119
2120 #[test]
2121 fn hierarchy_tree_max_depth() {
2122 let g = build_hierarchy_graph();
2123 let tree = g.hierarchy_tree("s1", 0).unwrap();
2125 assert!(tree.children.is_empty());
2126 let tree = g.hierarchy_tree("s1", 1).unwrap();
2128 assert_eq!(tree.children.len(), 2);
2129 assert!(tree.children.iter().all(|c| c.children.is_empty()));
2130 }
2131
2132 #[test]
2133 fn hierarchy_tree_missing_root() {
2134 let g = build_hierarchy_graph();
2135 assert!(g.hierarchy_tree("nonexistent", 10).is_none());
2136 }
2137
2138 #[test]
2141 fn classify_site() {
2142 let g = build_hierarchy_graph();
2143 assert_eq!(g.classify("s1").unwrap(), "site");
2144 }
2145
2146 #[test]
2147 fn classify_equip() {
2148 let mut g = EntityGraph::new();
2149 let mut d = HDict::new();
2150 d.set("id", Kind::Ref(HRef::from_val("ahu-1")));
2151 d.set("equip", Kind::Marker);
2152 d.set("ahu", Kind::Marker);
2153 g.add(d).unwrap();
2154 assert_eq!(g.classify("ahu-1").unwrap(), "ahu");
2155 }
2156
2157 #[test]
2158 fn classify_point() {
2159 let g = build_hierarchy_graph();
2160 assert_eq!(g.classify("p1").unwrap(), "sensor");
2162 }
2163
2164 #[test]
2165 fn classify_unknown() {
2166 let mut g = EntityGraph::new();
2167 let mut d = HDict::new();
2168 d.set("id", Kind::Ref(HRef::from_val("x1")));
2169 d.set("custom", Kind::Marker);
2170 g.add(d).unwrap();
2171 assert!(g.classify("x1").is_none());
2172 }
2173}