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    /// Edge-type ids present in the graph, sorted ascending.
230    /// `by_type` stays a `HashMap` (smaller than a BTreeMap migrate; snapshot encoding unchanged);
231    /// this method collect+sorts keys so iteration is deterministic.
232    pub fn etypes(&self) -> impl Iterator<Item = u32> + '_ {
233        let mut ids: Vec<u32> = self.by_type.keys().copied().collect();
234        ids.sort_unstable();
235        ids.into_iter()
236    }
237
238    /// Iterate all directed out-edges as `(etype, src, dst)` triples.
239    ///
240    /// Yields every edge in the overlay topology in arbitrary order.  Used by
241    /// callers (e.g. `core-api`) that need to walk the full edge set without
242    /// direct access to the private `by_type` field.
243    pub fn all_edges(&self) -> impl Iterator<Item = (u32, u32, u32)> + '_ {
244        self.by_type.iter().flat_map(|(&etype, adj)| {
245            adj.out.iter().flat_map(move |(&src, al)| {
246                al.merged()
247                    .into_owned()
248                    .into_iter()
249                    .map(move |dst| (etype, src, dst))
250            })
251        })
252    }
253
254    /// Remove an edge and return the outcome.
255    ///
256    /// - [`RemoveEdgeOutcome::RemovedFromOverlay`]: edge was in the overlay and has been
257    ///   removed; `edge_count` was decremented.
258    /// - [`RemoveEdgeOutcome::TombstonedBase`]: edge was NOT in the overlay; a tombstone
259    ///   was recorded in `out_tombstones` / `in_tombstones` so `TopologyView::neighbors`
260    ///   will subtract it from the base CSR.  The tombstone is harmless if the edge does
261    ///   not exist in the base CSR either.
262    pub fn remove_edge(&mut self, etype: u32, src: u32, dst: u32) -> RemoveEdgeOutcome {
263        let found_in_overlay = (|| {
264            let adj = self.by_type.get_mut(&etype)?;
265            let dsts = adj.out.get_mut(&src)?;
266            if !dsts.remove(dst) {
267                return None;
268            }
269            let srcs = adj
270                .inn
271                .get_mut(&dst)
272                .expect("invariant: inn bucket must exist when out contains dst");
273            assert!(
274                srcs.remove(src),
275                "invariant: inn must contain src when out contained dst"
276            );
277            self.edge_count -= 1;
278            Some(())
279        })()
280        .is_some();
281
282        if !found_in_overlay {
283            // The edge was not in the overlay (may be present only in the mmap'd base
284            // CSR).  Record a tombstone so `TopologyView::neighbors` can subtract it
285            // from the base when merging overlay + base for reads and for snapshot-merge.
286            self.out_tombstones
287                .entry(etype)
288                .or_default()
289                .entry(src)
290                .or_default()
291                .insert(dst);
292            self.in_tombstones
293                .entry(etype)
294                .or_default()
295                .entry(dst)
296                .or_default()
297                .insert(src);
298            RemoveEdgeOutcome::TombstonedBase
299        } else {
300            RemoveEdgeOutcome::RemovedFromOverlay
301        }
302    }
303
304    /// Return the set of `dst` ids tombstoned for `(etype, Direction::Out, src)`.
305    ///
306    /// Used by `TopologyView::neighbors` to subtract deleted edges from the base CSR.
307    pub fn out_tombstones_for(&self, etype: u32, src: u32) -> Option<&BTreeSet<u32>> {
308        self.out_tombstones.get(&etype)?.get(&src)
309    }
310
311    /// Return the set of `src` ids tombstoned for `(etype, Direction::In, dst)`.
312    pub fn in_tombstones_for(&self, etype: u32, dst: u32) -> Option<&BTreeSet<u32>> {
313        self.in_tombstones.get(&etype)?.get(&dst)
314    }
315
316    fn adj_list(&self, etype: u32, dir: Direction, v: u32) -> Option<&AdjList> {
317        self.by_type.get(&etype).and_then(|adj| match dir {
318            Direction::Out => adj.out.get(&v),
319            Direction::In => adj.inn.get(&v),
320        })
321    }
322
323    /// V7 packed CSR: etype count, then per etype (id, out map, in map), then edge_count.
324    /// Each adjacency map is vertex-count + (vertex, length-prefixed frozen neighbor array).
325    /// Deltas are merged into frozen on pack; unpack leaves deltas empty.
326    pub(crate) fn pack(&self, out: &mut Vec<u8>) {
327        let mut etypes: Vec<u32> = self.by_type.keys().copied().collect();
328        etypes.sort_unstable();
329        push_u32(out, etypes.len() as u32);
330        for et in etypes {
331            push_u32(out, et);
332            let adj = &self.by_type[&et];
333            pack_adj_map(out, &adj.out);
334            pack_adj_map(out, &adj.inn);
335        }
336        push_u64(out, self.edge_count);
337    }
338
339    pub(crate) fn unpack(src: &[u8]) -> StoreResult<(Self, usize)> {
340        let mut pos = 0usize;
341        let n_etypes = read_u32(src, &mut pos)? as usize;
342        let mut by_type = HashMap::with_capacity(n_etypes);
343        for _ in 0..n_etypes {
344            let et = read_u32(src, &mut pos)?;
345            let out = unpack_adj_map(src, &mut pos)?;
346            let inn = unpack_adj_map(src, &mut pos)?;
347            by_type.insert(et, TypedAdjacency { out, inn });
348        }
349        let edge_count = read_u64(src, &mut pos)?;
350        Ok((
351            Self {
352                by_type,
353                edge_count,
354                out_tombstones: HashMap::new(),
355                in_tombstones: HashMap::new(),
356            },
357            pos,
358        ))
359    }
360}
361
362fn pack_adj_map(out: &mut Vec<u8>, map: &HashMap<u32, AdjList>) {
363    let mut verts: Vec<u32> = map.keys().copied().collect();
364    verts.sort_unstable();
365    push_u32(out, verts.len() as u32);
366    for v in verts {
367        push_u32(out, v);
368        let list = &map[&v];
369        push_u32s(out, list.merged().as_ref());
370    }
371}
372
373fn unpack_adj_map(src: &[u8], pos: &mut usize) -> StoreResult<HashMap<u32, AdjList>> {
374    let n = read_u32(src, pos)? as usize;
375    let mut map = HashMap::with_capacity(n);
376    for _ in 0..n {
377        let v = read_u32(src, pos)?;
378        let frozen = read_u32s(src, pos)?;
379        map.insert(
380            v,
381            AdjList {
382                frozen,
383                delta: Vec::new(),
384            },
385        );
386    }
387    Ok(map)
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn edges_are_typed_directed_sorted_deduped() {
396        let mut t = Topology::new();
397        assert!(t.add_edge(0, 5, 9));
398        assert!(t.add_edge(0, 5, 3));
399        assert!(!t.add_edge(0, 5, 9)); // duplicate
400        assert!(t.add_edge(1, 5, 9)); // same pair, different type: distinct edge
401        assert_eq!(t.neighbors(0, Direction::Out, 5).as_ref(), &[3, 9]); // sorted
402        assert_eq!(t.neighbors(0, Direction::In, 9).as_ref(), &[5]);
403        assert_eq!(t.neighbors(0, Direction::Out, 999).as_ref(), &[] as &[u32]);
404        assert_eq!(t.degree(0, Direction::Out, 5), 2);
405        assert_eq!(t.edge_count(), 3);
406    }
407
408    #[test]
409    fn remove_edge_updates_both_sides_and_count() {
410        let mut t = Topology::new();
411        t.add_edge(0, 1, 2);
412        t.add_edge(0, 1, 3);
413        assert_eq!(
414            t.remove_edge(0, 1, 2),
415            RemoveEdgeOutcome::RemovedFromOverlay
416        );
417        assert_eq!(t.remove_edge(0, 1, 2), RemoveEdgeOutcome::TombstonedBase); // idempotent → tombstone
418        assert_eq!(t.remove_edge(9, 1, 2), RemoveEdgeOutcome::TombstonedBase); // unknown type → tombstone
419        assert_eq!(t.neighbors(0, Direction::Out, 1).as_ref(), &[3]);
420        assert_eq!(t.neighbors(0, Direction::In, 2).as_ref(), &[] as &[u32]);
421        assert_eq!(t.edge_count(), 1);
422        // re-add after remove works
423        assert!(t.add_edge(0, 1, 2));
424        assert_eq!(t.edge_count(), 2);
425    }
426
427    #[test]
428    fn etypes_empty_multiple_and_sorted() {
429        let empty = Topology::new();
430        assert_eq!(empty.etypes().collect::<Vec<_>>(), Vec::<u32>::new());
431
432        let mut t = Topology::new();
433        t.add_edge(3, 0, 1);
434        t.add_edge(1, 0, 1);
435        t.add_edge(3, 1, 2); // existing type, must not duplicate
436        t.add_edge(2, 0, 2);
437        assert_eq!(t.etypes().collect::<Vec<_>>(), vec![1, 2, 3]);
438    }
439
440    #[test]
441    fn insert_buffer_defers_sort_until_threshold_then_neighbors_sorted_unique() {
442        let mut t = Topology::new();
443        // Reverse inserts: a full sort on every add would keep the scan path borrowed.
444        for dst in (0u32..32).rev() {
445            assert!(t.add_edge(0, 0, dst));
446            let nbrs = t.neighbors(0, Direction::Out, 0);
447            assert!(
448                matches!(nbrs, Cow::Owned(_)),
449                "delta still dirty at {} edges; neighbors must take the owned merge path",
450                32 - dst
451            );
452            assert!(
453                nbrs.windows(2).all(|w| w[0] < w[1]),
454                "dirty merge must be sorted unique, got {nbrs:?}"
455            );
456            assert_eq!(nbrs.len(), (32 - dst) as usize);
457            let n = t.adj_list(0, Direction::Out, 0).unwrap();
458            assert!(
459                n.frozen.is_empty(),
460                "must not flush frozen before threshold"
461            );
462            assert_eq!(n.delta.len(), (32 - dst) as usize);
463        }
464        assert_eq!(t.degree(0, Direction::Out, 0), 32);
465        assert_eq!(t.neighbors(0, Direction::In, 31).as_ref(), &[0]);
466
467        assert!(t.add_edge(0, 0, 32)); // buffer len > 32 → merge into frozen
468        let nbrs = t.neighbors(0, Direction::Out, 0);
469        assert!(
470            matches!(nbrs, Cow::Borrowed(_)),
471            "after threshold flush, scan path must borrow the frozen block"
472        );
473        let expected: Vec<u32> = (0..33).collect();
474        assert_eq!(nbrs.as_ref(), expected.as_slice());
475        let n = t.adj_list(0, Direction::Out, 0).unwrap();
476        assert!(n.delta.is_empty());
477        assert_eq!(n.frozen, expected);
478        assert_eq!(t.edge_count(), 33);
479        assert_eq!(t.neighbors(0, Direction::In, 32).as_ref(), &[0]);
480        assert!(!t.add_edge(0, 0, 7));
481    }
482
483    #[test]
484    fn remove_edge_from_delta_and_from_frozen() {
485        let mut t = Topology::new();
486        for dst in 0u32..10 {
487            assert!(t.add_edge(0, 1, dst));
488        }
489        assert!(matches!(t.neighbors(0, Direction::Out, 1), Cow::Owned(_)));
490        assert_eq!(
491            t.remove_edge(0, 1, 7),
492            RemoveEdgeOutcome::RemovedFromOverlay
493        );
494        assert_eq!(t.remove_edge(0, 1, 7), RemoveEdgeOutcome::TombstonedBase);
495        assert_eq!(t.neighbors(0, Direction::In, 7).as_ref(), &[] as &[u32]);
496        assert_eq!(
497            t.neighbors(0, Direction::Out, 1).as_ref(),
498            &[0, 1, 2, 3, 4, 5, 6, 8, 9]
499        );
500        assert_eq!(t.edge_count(), 9);
501        assert!(t.add_edge(0, 1, 7));
502        assert_eq!(t.edge_count(), 10);
503
504        let mut t = Topology::new();
505        for dst in 0u32..33 {
506            assert!(t.add_edge(0, 1, dst));
507        }
508        assert!(matches!(
509            t.neighbors(0, Direction::Out, 1),
510            Cow::Borrowed(_)
511        ));
512        assert_eq!(
513            t.remove_edge(0, 1, 0),
514            RemoveEdgeOutcome::RemovedFromOverlay
515        );
516        assert_eq!(
517            t.remove_edge(0, 1, 32),
518            RemoveEdgeOutcome::RemovedFromOverlay
519        );
520        assert_eq!(t.edge_count(), 31);
521        assert_eq!(t.neighbors(0, Direction::In, 0).as_ref(), &[] as &[u32]);
522        assert_eq!(t.neighbors(0, Direction::In, 16).as_ref(), &[1]);
523        let expected: Vec<u32> = (1..32).collect();
524        assert_eq!(
525            t.neighbors(0, Direction::Out, 1).as_ref(),
526            expected.as_slice()
527        );
528        assert!(t.add_edge(0, 1, 0));
529        assert_eq!(t.edge_count(), 32);
530    }
531
532    #[test]
533    fn serde_wire_is_hashmap_of_hashmap_of_vec() {
534        #[derive(Serialize, Deserialize, PartialEq, Debug)]
535        struct WireAdj {
536            out: HashMap<u32, Vec<u32>>,
537            inn: HashMap<u32, Vec<u32>>,
538        }
539        #[derive(Serialize, Deserialize, PartialEq, Debug)]
540        struct Wire {
541            by_type: HashMap<u32, WireAdj>,
542            edge_count: u64,
543        }
544
545        let mut by_type = HashMap::new();
546        by_type.insert(
547            0,
548            WireAdj {
549                out: HashMap::from([(5, vec![3, 9])]),
550                inn: HashMap::from([(3, vec![5]), (9, vec![5])]),
551            },
552        );
553        let wire = Wire {
554            by_type,
555            edge_count: 2,
556        };
557        let encoded = bincode::serialize(&wire).unwrap();
558        let t: Topology = bincode::deserialize(&encoded).unwrap();
559        assert_eq!(t.neighbors(0, Direction::Out, 5).as_ref(), &[3, 9]);
560        assert_eq!(t.neighbors(0, Direction::In, 3).as_ref(), &[5]);
561        assert_eq!(t.neighbors(0, Direction::In, 9).as_ref(), &[5]);
562        assert_eq!(t.edge_count(), 2);
563        assert!(matches!(
564            t.neighbors(0, Direction::Out, 5),
565            Cow::Borrowed(_)
566        ));
567
568        let roundtrip: Wire = bincode::deserialize(&bincode::serialize(&t).unwrap()).unwrap();
569        assert_eq!(roundtrip.edge_count, 2);
570        assert_eq!(roundtrip.by_type[&0].out[&5], vec![3, 9]);
571        assert_eq!(roundtrip.by_type[&0].inn[&3], vec![5]);
572        assert_eq!(roundtrip.by_type[&0].inn[&9], vec![5]);
573
574        // Dirty delta still encodes as a sorted unique Vec.
575        let mut dirty = Topology::new();
576        for dst in (0u32..10).rev() {
577            dirty.add_edge(1, 0, dst);
578        }
579        assert!(matches!(
580            dirty.neighbors(1, Direction::Out, 0),
581            Cow::Owned(_)
582        ));
583        let dirty_wire: Wire = bincode::deserialize(&bincode::serialize(&dirty).unwrap()).unwrap();
584        assert_eq!(dirty_wire.edge_count, 10);
585        assert_eq!(
586            dirty_wire.by_type[&1].out[&0],
587            (0..10).collect::<Vec<u32>>()
588        );
589        for dst in 0..10 {
590            assert_eq!(dirty_wire.by_type[&1].inn[&dst], vec![0]);
591        }
592    }
593
594    #[test]
595    fn pack_roundtrip_merges_delta_and_restores_frozen() {
596        let mut t = Topology::new();
597        for dst in (0u32..10).rev() {
598            t.add_edge(2, 1, dst);
599        }
600        t.add_edge(0, 5, 9);
601        assert!(matches!(t.neighbors(2, Direction::Out, 1), Cow::Owned(_)));
602        let mut buf = Vec::new();
603        t.pack(&mut buf);
604        let (back, consumed) = Topology::unpack(&buf).unwrap();
605        assert_eq!(consumed, buf.len());
606        assert_eq!(back.edge_count(), 11);
607        let expected: Vec<u32> = (0..10).collect();
608        assert_eq!(
609            back.neighbors(2, Direction::Out, 1).as_ref(),
610            expected.as_slice()
611        );
612        assert!(matches!(
613            back.neighbors(2, Direction::Out, 1),
614            Cow::Borrowed(_)
615        ));
616        assert_eq!(back.neighbors(0, Direction::Out, 5).as_ref(), &[9]);
617        assert_eq!(back.neighbors(0, Direction::In, 9).as_ref(), &[5]);
618        assert_eq!(back.etypes().collect::<Vec<_>>(), vec![0, 2]);
619    }
620}