Skip to main content

core_storage/
topology.rs

1use crate::pack::{push_u32, push_u32s, push_u64, read_u32, read_u32s, read_u64};
2use crate::types::Result as StoreResult;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::borrow::Cow;
5use std::cmp::Ordering;
6use std::collections::{BTreeSet, HashMap};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum Direction {
10    Out,
11    In,
12}
13
14/// Outcome of [`Topology::remove_edge`], distinguishing the two "not removed from overlay"
15/// cases that have different semantics for callers aware of a base CSR.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum RemoveEdgeOutcome {
18    /// Edge was present in the overlay and has been removed. `edge_count` was decremented.
19    RemovedFromOverlay,
20    /// Edge was NOT present in the overlay. A tombstone has been recorded in both
21    /// `out_tombstones` and `in_tombstones` so `TopologyView::neighbors` will subtract
22    /// it from the base CSR during reads and snapshot-merges. The edge may or may not
23    /// exist in the base CSR — the tombstone is a no-op if it does not.
24    TombstonedBase,
25}
26
27/// Flush the per-vertex insert buffer into the frozen block once it exceeds this.
28const INSERT_BUFFER: usize = 32;
29
30/// Sorted frozen neighbors plus an unsorted insert buffer. Serializes as `Vec<u32>`
31/// (merged, sorted, unique) so V6 snapshots stay HashMap-of-HashMap-of-Vec.
32#[derive(Debug, Default, Clone)]
33pub(crate) struct AdjList {
34    pub(crate) frozen: Vec<u32>,
35    pub(crate) delta: Vec<u32>,
36}
37
38impl AdjList {
39    fn contains(&self, id: u32) -> bool {
40        self.frozen.binary_search(&id).is_ok() || self.delta.contains(&id)
41    }
42
43    fn push(&mut self, id: u32) {
44        self.delta.push(id);
45        if self.delta.len() > INSERT_BUFFER {
46            self.flush();
47        }
48    }
49
50    fn flush(&mut self) {
51        if self.delta.is_empty() {
52            return;
53        }
54        if self.frozen.is_empty() {
55            self.delta.sort_unstable();
56            self.delta.dedup();
57            std::mem::swap(&mut self.frozen, &mut self.delta);
58            return;
59        }
60        self.frozen = merge_sorted_unique(&self.frozen, &self.delta);
61        self.delta.clear();
62    }
63
64    fn is_empty(&self) -> bool {
65        self.frozen.is_empty() && self.delta.is_empty()
66    }
67
68    /// Return a sorted-unique merged view of frozen + delta.
69    ///
70    /// Borrows `frozen` directly when `delta` is empty (no allocation); allocates
71    /// only when delta is non-empty.
72    pub(crate) fn merged(&self) -> Cow<'_, [u32]> {
73        if self.delta.is_empty() {
74            Cow::Borrowed(&self.frozen)
75        } else {
76            Cow::Owned(merge_sorted_unique(&self.frozen, &self.delta))
77        }
78    }
79
80    fn remove(&mut self, id: u32) -> bool {
81        if let Ok(pos) = self.frozen.binary_search(&id) {
82            self.frozen.remove(pos);
83            return true;
84        }
85        if let Some(pos) = self.delta.iter().position(|&x| x == id) {
86            self.delta.swap_remove(pos);
87            return true;
88        }
89        false
90    }
91}
92
93impl Serialize for AdjList {
94    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
95        self.merged().serialize(serializer)
96    }
97}
98
99impl<'de> Deserialize<'de> for AdjList {
100    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
101        Ok(Self {
102            frozen: Vec::<u32>::deserialize(deserializer)?,
103            delta: Vec::new(),
104        })
105    }
106}
107
108fn merge_sorted_unique(frozen: &[u32], delta: &[u32]) -> Vec<u32> {
109    let mut extra: Vec<u32> = delta.to_vec();
110    extra.sort_unstable();
111    extra.dedup();
112    if frozen.is_empty() {
113        return extra;
114    }
115    if extra.is_empty() {
116        return frozen.to_vec();
117    }
118    let mut out = Vec::with_capacity(frozen.len() + extra.len());
119    let mut i = 0;
120    let mut j = 0;
121    while i < frozen.len() && j < extra.len() {
122        match frozen[i].cmp(&extra[j]) {
123            Ordering::Less => {
124                out.push(frozen[i]);
125                i += 1;
126            }
127            Ordering::Greater => {
128                out.push(extra[j]);
129                j += 1;
130            }
131            Ordering::Equal => {
132                out.push(frozen[i]);
133                i += 1;
134                j += 1;
135            }
136        }
137    }
138    out.extend_from_slice(&frozen[i..]);
139    out.extend_from_slice(&extra[j..]);
140    out
141}
142
143#[derive(Debug, Default, Clone, Serialize, Deserialize)]
144pub(crate) struct TypedAdjacency {
145    pub(crate) out: HashMap<u32, AdjList>,
146    pub(crate) inn: HashMap<u32, AdjList>,
147}
148
149/// Typed adjacency: per `(etype, dir, vertex)` a frozen sorted block plus an
150/// unsorted insert buffer. `neighbors` borrows the frozen block when the buffer
151/// is empty; otherwise it returns a sorted-unique merge.
152///
153/// On-disk (V6) shape is still `HashMap<u32, {out, inn: HashMap<u32, Vec<u32>>}>`.
154#[derive(Debug, Default, Clone, Serialize, Deserialize)]
155pub struct Topology {
156    pub(crate) by_type: HashMap<u32, TypedAdjacency>,
157    edge_count: u64,
158    /// Out-direction tombstones: edges deleted from the base CSR that are absent
159    /// from the overlay.  Keyed by `(etype → src → {deleted dst ids})`.
160    ///
161    /// Populated by `remove_edge` when the edge is not found in the overlay —
162    /// this records a deletion of a base-only edge so `TopologyView::neighbors`
163    /// can subtract it from the base CSR when merging.
164    ///
165    /// Not serialised: tombstones are eliminated at snapshot-merge time
166    /// (encode_v8 subtracts them when building the merged CSR section).
167    #[serde(skip)]
168    pub(crate) out_tombstones: HashMap<u32, HashMap<u32, BTreeSet<u32>>>,
169    /// In-direction tombstones: symmetric index of `out_tombstones`.
170    /// Keyed by `(etype → dst → {deleted src ids})` for O(log n) In lookups.
171    #[serde(skip)]
172    pub(crate) in_tombstones: HashMap<u32, HashMap<u32, BTreeSet<u32>>>,
173}
174
175impl Topology {
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    pub fn add_edge(&mut self, etype: u32, src: u32, dst: u32) -> bool {
181        let adj = self.by_type.entry(etype).or_default();
182        let dsts = adj.out.entry(src).or_default();
183        if dsts.contains(dst) {
184            return false;
185        }
186        dsts.push(dst);
187        let srcs = adj.inn.entry(dst).or_default();
188        assert!(
189            !srcs.contains(src),
190            "invariant: inn must not contain src as neighbor of dst when out lacks dst"
191        );
192        srcs.push(src);
193        self.edge_count += 1;
194        true
195    }
196
197    pub fn neighbors(&self, etype: u32, dir: Direction, v: u32) -> Cow<'_, [u32]> {
198        match self.adj_list(etype, dir, v) {
199            None => Cow::Borrowed(&[]),
200            Some(n) => n.merged(),
201        }
202    }
203
204    pub fn degree(&self, etype: u32, dir: Direction, v: u32) -> usize {
205        self.neighbors(etype, dir, v).as_ref().len()
206    }
207
208    /// Whether overlay adjacency for `(etype, dir, v)` has no neighbor ids.
209    ///
210    /// Used by [`crate::v8::seam::TopologyView::degree`] to take the archived
211    /// CSR-length fast path when this vertex has no overlay delta.
212    pub(crate) fn adj_is_empty(&self, etype: u32, dir: Direction, v: u32) -> bool {
213        self.adj_list(etype, dir, v).is_none_or(AdjList::is_empty)
214    }
215
216    /// Whether overlay tombstones for `(etype, dir, v)` are absent or empty.
217    pub(crate) fn tombstones_are_empty(&self, etype: u32, dir: Direction, v: u32) -> bool {
218        let set = match dir {
219            Direction::Out => self.out_tombstones_for(etype, v),
220            Direction::In => self.in_tombstones_for(etype, v),
221        };
222        set.is_none_or(|s| s.is_empty())
223    }
224
225    pub fn edge_count(&self) -> u64 {
226        self.edge_count
227    }
228
229    /// Number of base-edge tombstones in this overlay.
230    ///
231    /// Each tombstone records a deletion of an edge that lives in the
232    /// snapshot base, not in the overlay itself.  Used in the corrected
233    /// edge-count path that repairs a damaged base header (ledger row 107).
234    pub fn tombstone_count(&self) -> u64 {
235        self.out_tombstones
236            .values()
237            .flat_map(|m| m.values())
238            .map(|s| s.len() as u64)
239            .sum()
240    }
241
242    /// Edge-type ids present in the graph, sorted ascending.
243    /// `by_type` stays a `HashMap` (smaller than a BTreeMap migrate; snapshot encoding unchanged);
244    /// this method collect+sorts keys so iteration is deterministic.
245    pub fn etypes(&self) -> impl Iterator<Item = u32> + '_ {
246        let mut ids: Vec<u32> = self.by_type.keys().copied().collect();
247        ids.sort_unstable();
248        ids.into_iter()
249    }
250
251    /// Iterate all directed out-edges as `(etype, src, dst)` triples.
252    ///
253    /// Yields every edge in the overlay topology in arbitrary order.  Used by
254    /// callers (e.g. `core-api`) that need to walk the full edge set without
255    /// direct access to the private `by_type` field.
256    pub fn all_edges(&self) -> impl Iterator<Item = (u32, u32, u32)> + '_ {
257        self.by_type.iter().flat_map(|(&etype, adj)| {
258            adj.out.iter().flat_map(move |(&src, al)| {
259                al.merged()
260                    .into_owned()
261                    .into_iter()
262                    .map(move |dst| (etype, src, dst))
263            })
264        })
265    }
266
267    /// Remove an edge and return the outcome.
268    ///
269    /// - [`RemoveEdgeOutcome::RemovedFromOverlay`]: edge was in the overlay and has been
270    ///   removed; `edge_count` was decremented.
271    /// - [`RemoveEdgeOutcome::TombstonedBase`]: edge was NOT in the overlay; a tombstone
272    ///   was recorded in `out_tombstones` / `in_tombstones` so `TopologyView::neighbors`
273    ///   will subtract it from the base CSR.  The tombstone is harmless if the edge does
274    ///   not exist in the base CSR either.
275    pub fn remove_edge(&mut self, etype: u32, src: u32, dst: u32) -> RemoveEdgeOutcome {
276        let found_in_overlay = (|| {
277            let adj = self.by_type.get_mut(&etype)?;
278            let dsts = adj.out.get_mut(&src)?;
279            if !dsts.remove(dst) {
280                return None;
281            }
282            let srcs = adj
283                .inn
284                .get_mut(&dst)
285                .expect("invariant: inn bucket must exist when out contains dst");
286            assert!(
287                srcs.remove(src),
288                "invariant: inn must contain src when out contained dst"
289            );
290            self.edge_count -= 1;
291            Some(())
292        })()
293        .is_some();
294
295        if !found_in_overlay {
296            // The edge was not in the overlay (may be present only in the mmap'd base
297            // CSR).  Record a tombstone so `TopologyView::neighbors` can subtract it
298            // from the base when merging overlay + base for reads and for snapshot-merge.
299            self.out_tombstones
300                .entry(etype)
301                .or_default()
302                .entry(src)
303                .or_default()
304                .insert(dst);
305            self.in_tombstones
306                .entry(etype)
307                .or_default()
308                .entry(dst)
309                .or_default()
310                .insert(src);
311            RemoveEdgeOutcome::TombstonedBase
312        } else {
313            RemoveEdgeOutcome::RemovedFromOverlay
314        }
315    }
316
317    /// Return the set of `dst` ids tombstoned for `(etype, Direction::Out, src)`.
318    ///
319    /// Used by `TopologyView::neighbors` to subtract deleted edges from the base CSR.
320    pub fn out_tombstones_for(&self, etype: u32, src: u32) -> Option<&BTreeSet<u32>> {
321        self.out_tombstones.get(&etype)?.get(&src)
322    }
323
324    /// Return the set of `src` ids tombstoned for `(etype, Direction::In, dst)`.
325    pub fn in_tombstones_for(&self, etype: u32, dst: u32) -> Option<&BTreeSet<u32>> {
326        self.in_tombstones.get(&etype)?.get(&dst)
327    }
328
329    fn adj_list(&self, etype: u32, dir: Direction, v: u32) -> Option<&AdjList> {
330        self.by_type.get(&etype).and_then(|adj| match dir {
331            Direction::Out => adj.out.get(&v),
332            Direction::In => adj.inn.get(&v),
333        })
334    }
335
336    /// V7 packed CSR: etype count, then per etype (id, out map, in map), then edge_count.
337    /// Each adjacency map is vertex-count + (vertex, length-prefixed frozen neighbor array).
338    /// Deltas are merged into frozen on pack; unpack leaves deltas empty.
339    pub(crate) fn pack(&self, out: &mut Vec<u8>) {
340        let mut etypes: Vec<u32> = self.by_type.keys().copied().collect();
341        etypes.sort_unstable();
342        push_u32(out, etypes.len() as u32);
343        for et in etypes {
344            push_u32(out, et);
345            let adj = &self.by_type[&et];
346            pack_adj_map(out, &adj.out);
347            pack_adj_map(out, &adj.inn);
348        }
349        push_u64(out, self.edge_count);
350    }
351
352    pub(crate) fn unpack(src: &[u8]) -> StoreResult<(Self, usize)> {
353        let mut pos = 0usize;
354        let n_etypes = read_u32(src, &mut pos)? as usize;
355        let mut by_type = HashMap::with_capacity(n_etypes);
356        for _ in 0..n_etypes {
357            let et = read_u32(src, &mut pos)?;
358            let out = unpack_adj_map(src, &mut pos)?;
359            let inn = unpack_adj_map(src, &mut pos)?;
360            by_type.insert(et, TypedAdjacency { out, inn });
361        }
362        let edge_count = read_u64(src, &mut pos)?;
363        Ok((
364            Self {
365                by_type,
366                edge_count,
367                out_tombstones: HashMap::new(),
368                in_tombstones: HashMap::new(),
369            },
370            pos,
371        ))
372    }
373}
374
375fn pack_adj_map(out: &mut Vec<u8>, map: &HashMap<u32, AdjList>) {
376    let mut verts: Vec<u32> = map.keys().copied().collect();
377    verts.sort_unstable();
378    push_u32(out, verts.len() as u32);
379    for v in verts {
380        push_u32(out, v);
381        let list = &map[&v];
382        push_u32s(out, list.merged().as_ref());
383    }
384}
385
386fn unpack_adj_map(src: &[u8], pos: &mut usize) -> StoreResult<HashMap<u32, AdjList>> {
387    let n = read_u32(src, pos)? as usize;
388    let mut map = HashMap::with_capacity(n);
389    for _ in 0..n {
390        let v = read_u32(src, pos)?;
391        let frozen = read_u32s(src, pos)?;
392        map.insert(
393            v,
394            AdjList {
395                frozen,
396                delta: Vec::new(),
397            },
398        );
399    }
400    Ok(map)
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn edges_are_typed_directed_sorted_deduped() {
409        let mut t = Topology::new();
410        assert!(t.add_edge(0, 5, 9));
411        assert!(t.add_edge(0, 5, 3));
412        assert!(!t.add_edge(0, 5, 9)); // duplicate
413        assert!(t.add_edge(1, 5, 9)); // same pair, different type: distinct edge
414        assert_eq!(t.neighbors(0, Direction::Out, 5).as_ref(), &[3, 9]); // sorted
415        assert_eq!(t.neighbors(0, Direction::In, 9).as_ref(), &[5]);
416        assert_eq!(t.neighbors(0, Direction::Out, 999).as_ref(), &[] as &[u32]);
417        assert_eq!(t.degree(0, Direction::Out, 5), 2);
418        assert_eq!(t.edge_count(), 3);
419    }
420
421    #[test]
422    fn remove_edge_updates_both_sides_and_count() {
423        let mut t = Topology::new();
424        t.add_edge(0, 1, 2);
425        t.add_edge(0, 1, 3);
426        assert_eq!(
427            t.remove_edge(0, 1, 2),
428            RemoveEdgeOutcome::RemovedFromOverlay
429        );
430        assert_eq!(t.remove_edge(0, 1, 2), RemoveEdgeOutcome::TombstonedBase); // idempotent → tombstone
431        assert_eq!(t.remove_edge(9, 1, 2), RemoveEdgeOutcome::TombstonedBase); // unknown type → tombstone
432        assert_eq!(t.neighbors(0, Direction::Out, 1).as_ref(), &[3]);
433        assert_eq!(t.neighbors(0, Direction::In, 2).as_ref(), &[] as &[u32]);
434        assert_eq!(t.edge_count(), 1);
435        // re-add after remove works
436        assert!(t.add_edge(0, 1, 2));
437        assert_eq!(t.edge_count(), 2);
438    }
439
440    #[test]
441    fn etypes_empty_multiple_and_sorted() {
442        let empty = Topology::new();
443        assert_eq!(empty.etypes().collect::<Vec<_>>(), Vec::<u32>::new());
444
445        let mut t = Topology::new();
446        t.add_edge(3, 0, 1);
447        t.add_edge(1, 0, 1);
448        t.add_edge(3, 1, 2); // existing type, must not duplicate
449        t.add_edge(2, 0, 2);
450        assert_eq!(t.etypes().collect::<Vec<_>>(), vec![1, 2, 3]);
451    }
452
453    #[test]
454    fn insert_buffer_defers_sort_until_threshold_then_neighbors_sorted_unique() {
455        let mut t = Topology::new();
456        // Reverse inserts: a full sort on every add would keep the scan path borrowed.
457        for dst in (0u32..32).rev() {
458            assert!(t.add_edge(0, 0, dst));
459            let nbrs = t.neighbors(0, Direction::Out, 0);
460            assert!(
461                matches!(nbrs, Cow::Owned(_)),
462                "delta still dirty at {} edges; neighbors must take the owned merge path",
463                32 - dst
464            );
465            assert!(
466                nbrs.windows(2).all(|w| w[0] < w[1]),
467                "dirty merge must be sorted unique, got {nbrs:?}"
468            );
469            assert_eq!(nbrs.len(), (32 - dst) as usize);
470            let n = t.adj_list(0, Direction::Out, 0).unwrap();
471            assert!(
472                n.frozen.is_empty(),
473                "must not flush frozen before threshold"
474            );
475            assert_eq!(n.delta.len(), (32 - dst) as usize);
476        }
477        assert_eq!(t.degree(0, Direction::Out, 0), 32);
478        assert_eq!(t.neighbors(0, Direction::In, 31).as_ref(), &[0]);
479
480        assert!(t.add_edge(0, 0, 32)); // buffer len > 32 → merge into frozen
481        let nbrs = t.neighbors(0, Direction::Out, 0);
482        assert!(
483            matches!(nbrs, Cow::Borrowed(_)),
484            "after threshold flush, scan path must borrow the frozen block"
485        );
486        let expected: Vec<u32> = (0..33).collect();
487        assert_eq!(nbrs.as_ref(), expected.as_slice());
488        let n = t.adj_list(0, Direction::Out, 0).unwrap();
489        assert!(n.delta.is_empty());
490        assert_eq!(n.frozen, expected);
491        assert_eq!(t.edge_count(), 33);
492        assert_eq!(t.neighbors(0, Direction::In, 32).as_ref(), &[0]);
493        assert!(!t.add_edge(0, 0, 7));
494    }
495
496    #[test]
497    fn remove_edge_from_delta_and_from_frozen() {
498        let mut t = Topology::new();
499        for dst in 0u32..10 {
500            assert!(t.add_edge(0, 1, dst));
501        }
502        assert!(matches!(t.neighbors(0, Direction::Out, 1), Cow::Owned(_)));
503        assert_eq!(
504            t.remove_edge(0, 1, 7),
505            RemoveEdgeOutcome::RemovedFromOverlay
506        );
507        assert_eq!(t.remove_edge(0, 1, 7), RemoveEdgeOutcome::TombstonedBase);
508        assert_eq!(t.neighbors(0, Direction::In, 7).as_ref(), &[] as &[u32]);
509        assert_eq!(
510            t.neighbors(0, Direction::Out, 1).as_ref(),
511            &[0, 1, 2, 3, 4, 5, 6, 8, 9]
512        );
513        assert_eq!(t.edge_count(), 9);
514        assert!(t.add_edge(0, 1, 7));
515        assert_eq!(t.edge_count(), 10);
516
517        let mut t = Topology::new();
518        for dst in 0u32..33 {
519            assert!(t.add_edge(0, 1, dst));
520        }
521        assert!(matches!(
522            t.neighbors(0, Direction::Out, 1),
523            Cow::Borrowed(_)
524        ));
525        assert_eq!(
526            t.remove_edge(0, 1, 0),
527            RemoveEdgeOutcome::RemovedFromOverlay
528        );
529        assert_eq!(
530            t.remove_edge(0, 1, 32),
531            RemoveEdgeOutcome::RemovedFromOverlay
532        );
533        assert_eq!(t.edge_count(), 31);
534        assert_eq!(t.neighbors(0, Direction::In, 0).as_ref(), &[] as &[u32]);
535        assert_eq!(t.neighbors(0, Direction::In, 16).as_ref(), &[1]);
536        let expected: Vec<u32> = (1..32).collect();
537        assert_eq!(
538            t.neighbors(0, Direction::Out, 1).as_ref(),
539            expected.as_slice()
540        );
541        assert!(t.add_edge(0, 1, 0));
542        assert_eq!(t.edge_count(), 32);
543    }
544
545    #[test]
546    fn serde_wire_is_hashmap_of_hashmap_of_vec() {
547        #[derive(Serialize, Deserialize, PartialEq, Debug)]
548        struct WireAdj {
549            out: HashMap<u32, Vec<u32>>,
550            inn: HashMap<u32, Vec<u32>>,
551        }
552        #[derive(Serialize, Deserialize, PartialEq, Debug)]
553        struct Wire {
554            by_type: HashMap<u32, WireAdj>,
555            edge_count: u64,
556        }
557
558        let mut by_type = HashMap::new();
559        by_type.insert(
560            0,
561            WireAdj {
562                out: HashMap::from([(5, vec![3, 9])]),
563                inn: HashMap::from([(3, vec![5]), (9, vec![5])]),
564            },
565        );
566        let wire = Wire {
567            by_type,
568            edge_count: 2,
569        };
570        let encoded = bincode::serialize(&wire).unwrap();
571        let t: Topology = bincode::deserialize(&encoded).unwrap();
572        assert_eq!(t.neighbors(0, Direction::Out, 5).as_ref(), &[3, 9]);
573        assert_eq!(t.neighbors(0, Direction::In, 3).as_ref(), &[5]);
574        assert_eq!(t.neighbors(0, Direction::In, 9).as_ref(), &[5]);
575        assert_eq!(t.edge_count(), 2);
576        assert!(matches!(
577            t.neighbors(0, Direction::Out, 5),
578            Cow::Borrowed(_)
579        ));
580
581        let roundtrip: Wire = bincode::deserialize(&bincode::serialize(&t).unwrap()).unwrap();
582        assert_eq!(roundtrip.edge_count, 2);
583        assert_eq!(roundtrip.by_type[&0].out[&5], vec![3, 9]);
584        assert_eq!(roundtrip.by_type[&0].inn[&3], vec![5]);
585        assert_eq!(roundtrip.by_type[&0].inn[&9], vec![5]);
586
587        // Dirty delta still encodes as a sorted unique Vec.
588        let mut dirty = Topology::new();
589        for dst in (0u32..10).rev() {
590            dirty.add_edge(1, 0, dst);
591        }
592        assert!(matches!(
593            dirty.neighbors(1, Direction::Out, 0),
594            Cow::Owned(_)
595        ));
596        let dirty_wire: Wire = bincode::deserialize(&bincode::serialize(&dirty).unwrap()).unwrap();
597        assert_eq!(dirty_wire.edge_count, 10);
598        assert_eq!(
599            dirty_wire.by_type[&1].out[&0],
600            (0..10).collect::<Vec<u32>>()
601        );
602        for dst in 0..10 {
603            assert_eq!(dirty_wire.by_type[&1].inn[&dst], vec![0]);
604        }
605    }
606
607    #[test]
608    fn pack_roundtrip_merges_delta_and_restores_frozen() {
609        let mut t = Topology::new();
610        for dst in (0u32..10).rev() {
611            t.add_edge(2, 1, dst);
612        }
613        t.add_edge(0, 5, 9);
614        assert!(matches!(t.neighbors(2, Direction::Out, 1), Cow::Owned(_)));
615        let mut buf = Vec::new();
616        t.pack(&mut buf);
617        let (back, consumed) = Topology::unpack(&buf).unwrap();
618        assert_eq!(consumed, buf.len());
619        assert_eq!(back.edge_count(), 11);
620        let expected: Vec<u32> = (0..10).collect();
621        assert_eq!(
622            back.neighbors(2, Direction::Out, 1).as_ref(),
623            expected.as_slice()
624        );
625        assert!(matches!(
626            back.neighbors(2, Direction::Out, 1),
627            Cow::Borrowed(_)
628        ));
629        assert_eq!(back.neighbors(0, Direction::Out, 5).as_ref(), &[9]);
630        assert_eq!(back.neighbors(0, Direction::In, 9).as_ref(), &[5]);
631        assert_eq!(back.etypes().collect::<Vec<_>>(), vec![0, 2]);
632    }
633}