1use alloc::collections::BTreeMap;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::object_cache::ObjectId;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum RelationshipKind {
17 Reference,
20 Composition,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum Direction {
27 Mono,
29 Bi,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum CascadeMode {
36 None,
38 Update,
40 Delete,
42 All,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Relationship {
49 pub name: String,
51 pub source: ObjectId,
53 pub target: ObjectId,
55 pub kind: RelationshipKind,
57 pub direction: Direction,
59 pub cascade: CascadeMode,
61}
62
63#[derive(Debug, Default, Clone, PartialEq, Eq)]
65pub struct RelationshipResolver {
66 by_source: BTreeMap<ObjectId, Vec<Relationship>>,
67}
68
69impl RelationshipResolver {
70 #[must_use]
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn add(&mut self, rel: Relationship) {
78 let source = rel.source.clone();
79 self.by_source.entry(source).or_default().push(rel.clone());
80 if rel.direction == Direction::Bi {
81 let mut reverse = rel;
82 core::mem::swap(&mut reverse.source, &mut reverse.target);
83 self.by_source
84 .entry(reverse.source.clone())
85 .or_default()
86 .push(reverse);
87 }
88 }
89
90 #[must_use]
92 pub fn outgoing(&self, source: &ObjectId) -> Vec<&Relationship> {
93 self.by_source
94 .get(source)
95 .map_or_else(Vec::new, |v| v.iter().collect())
96 }
97
98 #[must_use]
100 pub fn len(&self) -> usize {
101 self.by_source.values().map(Vec::len).sum()
102 }
103
104 #[must_use]
106 pub fn is_empty(&self) -> bool {
107 self.by_source.is_empty()
108 }
109
110 #[must_use]
113 pub fn cascade_targets_for_delete(&self, source: &ObjectId) -> Vec<ObjectId> {
114 self.by_source.get(source).map_or_else(Vec::new, |rels| {
115 rels.iter()
116 .filter(|r| matches!(r.cascade, CascadeMode::Delete | CascadeMode::All))
117 .map(|r| r.target.clone())
118 .collect()
119 })
120 }
121
122 #[must_use]
124 pub fn cascade_targets_for_update(&self, source: &ObjectId) -> Vec<ObjectId> {
125 self.by_source.get(source).map_or_else(Vec::new, |rels| {
126 rels.iter()
127 .filter(|r| matches!(r.cascade, CascadeMode::Update | CascadeMode::All))
128 .map(|r| r.target.clone())
129 .collect()
130 })
131 }
132}
133
134#[cfg(test)]
135#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
136mod tests {
137 use super::*;
138
139 fn oid(t: &str, k: &[u8]) -> ObjectId {
140 ObjectId::new(t.into(), k.to_vec())
141 }
142
143 fn rel(name: &str, src: ObjectId, tgt: ObjectId) -> Relationship {
144 Relationship {
145 name: name.into(),
146 source: src,
147 target: tgt,
148 kind: RelationshipKind::Reference,
149 direction: Direction::Mono,
150 cascade: CascadeMode::None,
151 }
152 }
153
154 #[test]
155 fn mono_adds_one_entry() {
156 let mut r = RelationshipResolver::new();
157 r.add(rel("a", oid("T", b"1"), oid("T", b"2")));
158 assert_eq!(r.len(), 1);
159 assert_eq!(r.outgoing(&oid("T", b"1")).len(), 1);
160 assert_eq!(r.outgoing(&oid("T", b"2")).len(), 0);
161 }
162
163 #[test]
164 fn bi_adds_inverse() {
165 let mut r = RelationshipResolver::new();
166 let mut x = rel("a", oid("T", b"1"), oid("T", b"2"));
167 x.direction = Direction::Bi;
168 r.add(x);
169 assert_eq!(r.len(), 2);
170 assert_eq!(r.outgoing(&oid("T", b"1")).len(), 1);
171 assert_eq!(r.outgoing(&oid("T", b"2")).len(), 1);
172 }
173
174 #[test]
175 fn cascade_delete_targets_only_marked_relations() {
176 let mut r = RelationshipResolver::new();
177 let mut a = rel("a", oid("T", b"1"), oid("T", b"2"));
178 a.cascade = CascadeMode::Delete;
179 let b = rel("b", oid("T", b"1"), oid("T", b"3"));
180 r.add(a);
181 r.add(b);
182 let targets = r.cascade_targets_for_delete(&oid("T", b"1"));
183 assert_eq!(targets, alloc::vec![oid("T", b"2")]);
184 }
185
186 #[test]
187 fn cascade_update_targets_only_marked_relations() {
188 let mut r = RelationshipResolver::new();
189 let mut a = rel("a", oid("T", b"1"), oid("T", b"2"));
190 a.cascade = CascadeMode::Update;
191 r.add(a);
192 let mut b = rel("b", oid("T", b"1"), oid("T", b"3"));
193 b.cascade = CascadeMode::All;
194 r.add(b);
195 let targets = r.cascade_targets_for_update(&oid("T", b"1"));
196 assert_eq!(targets.len(), 2);
197 }
198
199 #[test]
200 fn relationship_kind_distinct() {
201 assert_ne!(RelationshipKind::Reference, RelationshipKind::Composition);
202 }
203
204 #[test]
205 fn cascade_modes_distinct() {
206 assert_ne!(CascadeMode::None, CascadeMode::All);
207 assert_ne!(CascadeMode::Update, CascadeMode::Delete);
208 }
209
210 #[test]
211 fn empty_resolver_returns_empty_lists() {
212 let r = RelationshipResolver::new();
213 assert!(r.is_empty());
214 assert!(r.cascade_targets_for_delete(&oid("T", b"x")).is_empty());
215 assert!(r.cascade_targets_for_update(&oid("T", b"x")).is_empty());
216 }
217}