aranya_runtime/client/
transaction.rs

1use alloc::collections::{BTreeMap, VecDeque};
2use core::{marker::PhantomData, mem};
3
4use aranya_buggy::{bug, BugExt};
5
6use crate::{
7    Address, ClientError, Command, CommandId, CommandRecall, Engine, EngineError, GraphId,
8    Location, MergeIds, PeerCache, Perspective, Policy, PolicyId, Prior, Revertable, Segment, Sink,
9    Storage, StorageError, StorageProvider, MAX_COMMAND_LENGTH,
10};
11
12/// Transaction used to receive many commands at once.
13///
14/// The transaction allows us to have many temporary heads at once, so we don't
15/// need as many merges when adding commands. When the transaction is committed,
16/// we will merge all temporary heads and the storage head, and then commit the
17/// result as the new storage head.
18pub struct Transaction<SP: StorageProvider, E> {
19    /// The ID of the associated storage
20    storage_id: GraphId,
21    /// Current working perspective
22    perspective: Option<SP::Perspective>,
23    /// Head of the current perspective
24    phead: Option<CommandId>,
25    /// Written but not committed heads
26    heads: BTreeMap<Address, Location>,
27    /// Tag for associated engine
28    _engine: PhantomData<E>,
29}
30
31impl<SP: StorageProvider, E> Transaction<SP, E> {
32    pub(super) const fn new(storage_id: GraphId) -> Self {
33        Self {
34            storage_id,
35            perspective: None,
36            phead: None,
37            heads: BTreeMap::new(),
38            _engine: PhantomData,
39        }
40    }
41}
42
43impl<SP: StorageProvider, E: Engine> Transaction<SP, E> {
44    /// Find a given id if reachable within this transaction.
45    ///
46    /// Does not search `self.perspective`, which should be written out beforehand.
47    fn locate(
48        &self,
49        storage: &mut SP::Storage,
50        address: Address,
51    ) -> Result<Option<Location>, ClientError> {
52        // Search from committed head.
53        if let Some(found) = storage.get_location(address)? {
54            return Ok(Some(found));
55        }
56        // Search from our temporary heads.
57        for &head in self.heads.values() {
58            if let Some(found) = storage.get_location_from(head, address)? {
59                return Ok(Some(found));
60            }
61        }
62        Ok(None)
63    }
64
65    /// Write current perspective, merge transaction heads, and commit to graph.
66    pub(super) fn commit(
67        &mut self,
68        provider: &mut SP,
69        engine: &mut E,
70        sink: &mut impl Sink<E::Effect>,
71    ) -> Result<(), ClientError> {
72        let storage = provider.get_storage(self.storage_id)?;
73
74        // Write out current perspective.
75        if let Some(p) = Option::take(&mut self.perspective) {
76            self.phead = None;
77            let segment = storage.write(p)?;
78            let head = segment.head()?;
79            self.heads.insert(head.address()?, segment.head_location());
80        }
81
82        // Merge heads pairwise until single head left, then commit.
83        // TODO(#370): Merge deterministically
84        let mut heads: VecDeque<_> = mem::take(&mut self.heads).into_iter().collect();
85        let mut merging_head = false;
86        while let Some((left_id, mut left_loc)) = heads.pop_front() {
87            if let Some((right_id, mut right_loc)) = heads.pop_front() {
88                let (policy, policy_id) = choose_policy(storage, engine, left_loc, right_loc)?;
89
90                let mut buffer = [0u8; MAX_COMMAND_LENGTH];
91                let merge_ids = MergeIds::new(left_id, right_id).assume("merging different ids")?;
92                if left_id > right_id {
93                    mem::swap(&mut left_loc, &mut right_loc);
94                }
95                let command = policy.merge(&mut buffer, merge_ids)?;
96
97                let (braid, last_common_ancestor) =
98                    make_braid_segment::<_, E>(storage, left_loc, right_loc, sink, policy)?;
99
100                let mut perspective = storage
101                    .new_merge_perspective(
102                        left_loc,
103                        right_loc,
104                        last_common_ancestor,
105                        policy_id,
106                        braid,
107                    )?
108                    .assume("trx heads should exist in storage")?;
109                perspective.add_command(&command)?;
110
111                let segment = storage.write(perspective)?;
112                let head = segment.head()?;
113
114                heads.push_back((head.address()?, segment.head_location()));
115            } else {
116                let segment = storage.get_segment(left_loc)?;
117                // Try to commit. If it fails with `HeadNotAncestor`, we know we
118                // need to merge with the graph head.
119                match storage.commit(segment) {
120                    Ok(()) => break,
121                    Err(StorageError::HeadNotAncestor) => {
122                        if merging_head {
123                            bug!("merging with graph head again, would loop");
124                        }
125
126                        merging_head = true;
127
128                        heads.push_back((left_id, left_loc));
129
130                        let head_loc = storage.get_head()?;
131                        let segment = storage.get_segment(head_loc)?;
132                        let head = segment.head()?;
133                        heads.push_back((head.address()?, segment.head_location()));
134                    }
135                    Err(e) => return Err(e.into()),
136                }
137            }
138        }
139
140        Ok(())
141    }
142
143    /// Attempt to store the `command` in the graph with `storage_id`. Effects will be
144    /// emitted to the `sink`. This interface is used when syncing with another device
145    /// and integrating the new commands.
146    pub(super) fn add_commands(
147        &mut self,
148        commands: &[impl Command],
149        provider: &mut SP,
150        engine: &mut E,
151        sink: &mut impl Sink<E::Effect>,
152        request_heads: &mut PeerCache,
153    ) -> Result<usize, ClientError> {
154        let mut commands = commands.iter();
155        let mut count: usize = 0;
156
157        // Get storage or try to initialize with first command.
158        let storage = match provider.get_storage(self.storage_id) {
159            Ok(s) => s,
160            Err(StorageError::NoSuchStorage) => {
161                let command = commands.next().ok_or(ClientError::InitError)?;
162                count = count.checked_add(1).assume("must not overflow")?;
163                self.init(command, engine, provider, sink)?
164            }
165            Err(e) => return Err(e.into()),
166        };
167
168        // Handle remaining commands.
169        for command in commands {
170            if self
171                .perspective
172                .as_ref()
173                .is_some_and(|p| p.includes(command.id()))
174            {
175                // Command in current perspective.
176                continue;
177            }
178            if let Some(loc) = self.locate(storage, command.address()?)? {
179                request_heads.add_command(storage, command.address()?, loc)?;
180                // Command already added.
181                continue;
182            }
183            match command.parent() {
184                Prior::None => {
185                    if command.id().into_id() == self.storage_id.into_id() {
186                        // Graph already initialized, extra init just spurious
187                    } else {
188                        bug!("init command does not belong in graph");
189                    }
190                }
191                Prior::Single(parent) => {
192                    self.add_single(storage, engine, sink, command, parent)?;
193                    count = count.checked_add(1).assume("must not overflow")?;
194                }
195                Prior::Merge(left, right) => {
196                    self.add_merge(storage, engine, sink, command, left, right)?;
197                    count = count.checked_add(1).assume("must not overflow")?;
198                }
199            };
200            if let Some(loc) = self.locate(storage, command.address()?)? {
201                request_heads.add_command(storage, command.address()?, loc)?;
202            }
203        }
204        let head_location = storage.get_head()?;
205        let cmd_seg = storage.get_segment(head_location)?;
206        let command = cmd_seg.head()?;
207        request_heads.add_command(storage, command.address()?, head_location)?;
208
209        Ok(count)
210    }
211
212    fn add_single(
213        &mut self,
214        storage: &mut <SP as StorageProvider>::Storage,
215        engine: &mut E,
216        sink: &mut impl Sink<E::Effect>,
217        command: &impl Command,
218        parent: Address,
219    ) -> Result<(), ClientError> {
220        let perspective = self.get_perspective(parent, storage)?;
221
222        let policy_id = perspective.policy();
223        let policy = engine.get_policy(policy_id)?;
224
225        // Try to run command, or revert if failed.
226        sink.begin();
227        let checkpoint = perspective.checkpoint();
228        if let Err(e) = policy.call_rule(command, perspective, sink, CommandRecall::None) {
229            perspective.revert(checkpoint)?;
230            sink.rollback();
231            return Err(e.into());
232        }
233        perspective.add_command(command)?;
234        sink.commit();
235
236        self.phead = Some(command.id());
237
238        Ok(())
239    }
240
241    fn add_merge(
242        &mut self,
243        storage: &mut <SP as StorageProvider>::Storage,
244        engine: &mut E,
245        sink: &mut impl Sink<E::Effect>,
246        command: &impl Command,
247        left: Address,
248        right: Address,
249    ) -> Result<bool, ClientError> {
250        // Must always start a new perspective for merges.
251        if let Some(p) = Option::take(&mut self.perspective) {
252            let seg = storage.write(p)?;
253            let head = seg.head()?;
254            self.heads.insert(head.address()?, seg.head_location());
255        }
256
257        let left_loc = self
258            .locate(storage, left)?
259            .ok_or(ClientError::NoSuchParent(left.id))?;
260        let right_loc = self
261            .locate(storage, right)?
262            .ok_or(ClientError::NoSuchParent(right.id))?;
263
264        let (policy, policy_id) = choose_policy(storage, engine, left_loc, right_loc)?;
265
266        // Braid commands from left and right into an ordered sequence.
267        let (braid, last_common_ancestor) =
268            make_braid_segment::<_, E>(storage, left_loc, right_loc, sink, policy)?;
269
270        let mut perspective = storage
271            .new_merge_perspective(left_loc, right_loc, last_common_ancestor, policy_id, braid)?
272            .assume(
273                "we already found left and right locations above and we only call this with merge command",
274            )?;
275        perspective.add_command(command)?;
276
277        // These are no longer heads of the transaction, since they are both covered by the merge
278        self.heads.remove(&left);
279        self.heads.remove(&right);
280
281        self.perspective = Some(perspective);
282        self.phead = Some(command.id());
283
284        Ok(true)
285    }
286
287    /// Get a perspective to which we can add a command with the given parant.
288    ///
289    /// If parent is the head of the current perspective, we can just use it.
290    /// Otherwise, we must write out the perspective and get a new one.
291    fn get_perspective(
292        &mut self,
293        parent: Address,
294        storage: &mut <SP as StorageProvider>::Storage,
295    ) -> Result<&mut <SP as StorageProvider>::Perspective, ClientError> {
296        if self.phead == Some(parent.id) {
297            // Command will append to current perspective.
298            return Ok(self
299                .perspective
300                .as_mut()
301                .assume("trx has perspective when has phead")?);
302        }
303
304        // Write out the current perspective.
305        if let Some(p) = Option::take(&mut self.perspective) {
306            self.phead = None;
307            let seg = storage.write(p)?;
308            let head = seg.head()?;
309            self.heads.insert(head.address()?, seg.head_location());
310        }
311
312        let loc = self
313            .locate(storage, parent)?
314            .ok_or(ClientError::NoSuchParent(parent.id))?;
315
316        // Get a new perspective and store it in the transaction.
317        let p = self.perspective.insert(
318            storage
319                .get_linear_perspective(loc)?
320                .assume("location should already be in storage")?,
321        );
322
323        self.phead = Some(parent.id);
324        self.heads.remove(&parent);
325
326        Ok(p)
327    }
328
329    fn init<'sp>(
330        &mut self,
331        command: &impl Command,
332        engine: &mut E,
333        provider: &'sp mut SP,
334        sink: &mut impl Sink<E::Effect>,
335    ) -> Result<&'sp mut <SP as StorageProvider>::Storage, ClientError> {
336        // Storage ID is the id of the init command by definition.
337        if self.storage_id.into_id() != command.id().into_id() {
338            return Err(ClientError::InitError);
339        }
340
341        // The init command must not have a parent.
342        if !matches!(command.parent(), Prior::None) {
343            return Err(ClientError::InitError);
344        }
345
346        // The graph must have policy to start with.
347        let Some(policy_data) = command.policy() else {
348            return Err(ClientError::InitError);
349        };
350
351        let policy_id = engine.add_policy(policy_data)?;
352        let policy = engine.get_policy(policy_id)?;
353
354        // Get an empty perspective and run the init command.
355        let mut perspective = provider.new_perspective(policy_id);
356        sink.begin();
357        if let Err(e) = policy.call_rule(command, &mut perspective, sink, CommandRecall::None) {
358            sink.rollback();
359            // We don't need to revert perspective since we just drop it.
360            return Err(e.into());
361        }
362        perspective.add_command(command)?;
363
364        let (_, storage) = provider.new_storage(perspective)?;
365
366        // Wait to commit until we are absolutely sure we've initialized.
367        sink.commit();
368
369        Ok(storage)
370    }
371}
372
373/// Run the braid algorithm and evaluate the sequence to create a braided fact index.
374fn make_braid_segment<S: Storage, E: Engine>(
375    storage: &mut S,
376    left: Location,
377    right: Location,
378    sink: &mut impl Sink<E::Effect>,
379    policy: &E::Policy,
380) -> Result<(S::FactIndex, (Location, usize)), ClientError> {
381    let order = super::braid(storage, left, right)?;
382    let last_common_ancestor = super::last_common_ancestor(storage, left, right)?;
383
384    let (&first, rest) = order.split_first().assume("braid is non-empty")?;
385
386    let mut braid_perspective = storage.get_fact_perspective(first)?;
387
388    sink.begin();
389
390    for &location in rest {
391        let segment = storage.get_segment(location)?;
392        let command = segment
393            .get_command(location)
394            .assume("braid only contains existing commands")?;
395
396        let result = policy.call_rule(
397            &command,
398            &mut braid_perspective,
399            sink,
400            CommandRecall::OnCheck,
401        );
402
403        // If the command failed in an uncontrolled way, rollback
404        if let Err(e) = result {
405            if e != EngineError::Check {
406                sink.rollback();
407                return Err(e.into());
408            }
409        }
410    }
411
412    let braid = storage.write_facts(braid_perspective)?;
413
414    sink.commit();
415
416    Ok((braid, last_common_ancestor))
417}
418
419/// Select the policy from two locations with the greatest serial value.
420fn choose_policy<'a, E: Engine>(
421    storage: &impl Storage,
422    engine: &'a E,
423    left: Location,
424    right: Location,
425) -> Result<(&'a E::Policy, PolicyId), ClientError> {
426    Ok(core::cmp::max_by_key(
427        get_policy(storage, engine, left)?,
428        get_policy(storage, engine, right)?,
429        |(p, _)| p.serial(),
430    ))
431}
432
433fn get_policy<'a, E: Engine>(
434    storage: &impl Storage,
435    engine: &'a E,
436    location: Location,
437) -> Result<(&'a E::Policy, PolicyId), ClientError> {
438    let segment = storage.get_segment(location)?;
439    let policy_id = segment.policy();
440    let policy = engine.get_policy(policy_id)?;
441    Ok((policy, policy_id))
442}
443
444#[cfg(test)]
445mod test {
446    use aranya_buggy::Bug;
447    use test_log::test;
448
449    use super::*;
450    use crate::{memory::MemStorageProvider, ClientState, Keys, MergeIds, Priority};
451
452    struct SeqEngine;
453
454    /// [`SeqPolicy`] is a very simple policy which appends the id of each
455    /// command to a fact named `b"seq"`. At each point in the graph, the value
456    /// of this fact should be equal to the ids in braid order of all facts up
457    /// to that point.
458    struct SeqPolicy;
459
460    struct SeqCommand {
461        id: CommandId,
462        prior: Prior<Address>,
463        data: Box<str>,
464        max_cut: usize,
465    }
466
467    impl Engine for SeqEngine {
468        type Policy = SeqPolicy;
469        type Effect = ();
470
471        fn add_policy(&mut self, _policy: &[u8]) -> Result<PolicyId, EngineError> {
472            Ok(PolicyId::new(0))
473        }
474
475        fn get_policy(&self, _id: PolicyId) -> Result<&Self::Policy, EngineError> {
476            Ok(&SeqPolicy)
477        }
478    }
479
480    impl Policy for SeqPolicy {
481        type Action<'a> = &'a str;
482        type Effect = ();
483        type Command<'a> = SeqCommand;
484
485        fn serial(&self) -> u32 {
486            0
487        }
488
489        fn call_rule(
490            &self,
491            command: &impl Command,
492            facts: &mut impl crate::FactPerspective,
493            _sink: &mut impl Sink<Self::Effect>,
494            _recall: CommandRecall,
495        ) -> Result<(), EngineError> {
496            assert!(
497                !matches!(command.parent(), Prior::Merge { .. }),
498                "merges shouldn't be evaluated"
499            );
500
501            // For init and basic commands, append the id to the seq fact.
502            let data = command.bytes();
503            if let Some(seq) = facts.query("seq", &Keys::default())?.as_deref() {
504                facts.insert(
505                    "seq".into(),
506                    Keys::default(),
507                    [seq, b":", data].concat().into(),
508                );
509            } else {
510                facts.insert("seq".into(), Keys::default(), data.into());
511            };
512            Ok(())
513        }
514
515        fn call_action(
516            &self,
517            _action: Self::Action<'_>,
518            _facts: &mut impl Perspective,
519            _sink: &mut impl Sink<Self::Effect>,
520        ) -> Result<(), EngineError> {
521            unimplemented!()
522        }
523
524        fn merge<'a>(
525            &self,
526            _target: &'a mut [u8],
527            ids: MergeIds,
528        ) -> Result<Self::Command<'a>, EngineError> {
529            let (left, right): (Address, Address) = ids.into();
530            let mut buf = [0u8; 128];
531            buf[..64].copy_from_slice(left.id.as_bytes());
532            buf[64..].copy_from_slice(right.id.as_bytes());
533            let id = CommandId::hash_for_testing_only(&buf);
534
535            Ok(SeqCommand::new(
536                id,
537                Prior::Merge(left, right),
538                left.max_cut
539                    .max(right.max_cut)
540                    .checked_add(1)
541                    .assume("must not overflow")?,
542            ))
543        }
544    }
545
546    impl SeqCommand {
547        fn new(id: CommandId, prior: Prior<Address>, max_cut: usize) -> Self {
548            let data = id.short_b58().into_boxed_str();
549            Self {
550                id,
551                prior,
552                data,
553                max_cut,
554            }
555        }
556    }
557
558    impl Command for SeqCommand {
559        fn priority(&self) -> Priority {
560            match self.prior {
561                Prior::None => Priority::Init,
562                Prior::Single(_) => {
563                    // Use the last byte of the ID as priority, just so we can
564                    // properly see the effects of braiding
565                    let id = self.id.as_bytes();
566                    let priority = u32::from(*id.last().unwrap());
567                    Priority::Basic(priority)
568                }
569                Prior::Merge(_, _) => Priority::Merge,
570            }
571        }
572
573        fn id(&self) -> CommandId {
574            self.id
575        }
576
577        fn parent(&self) -> Prior<Address> {
578            self.prior
579        }
580
581        fn policy(&self) -> Option<&[u8]> {
582            // We don't actually need any policy bytes, but the
583            // transaction/storage requires it on init commands.
584            match self.prior {
585                Prior::None { .. } => Some(b""),
586                _ => None,
587            }
588        }
589
590        fn bytes(&self) -> &[u8] {
591            self.data.as_bytes()
592        }
593
594        fn max_cut(&self) -> Result<usize, Bug> {
595            Ok(self.max_cut)
596        }
597    }
598
599    struct NullSink;
600    impl<E> Sink<E> for NullSink {
601        fn begin(&mut self) {}
602        fn consume(&mut self, _: E) {}
603        fn rollback(&mut self) {}
604        fn commit(&mut self) {}
605    }
606
607    /// [`GraphBuilder`] and the associated macro [`graph`] provide an easy way
608    /// to create a graph with a specific structure.
609    struct GraphBuilder<SP: StorageProvider> {
610        client: ClientState<SeqEngine, SP>,
611        trx: Transaction<SP, SeqEngine>,
612    }
613
614    impl<SP: StorageProvider> GraphBuilder<SP> {
615        pub fn init(mut client: ClientState<SeqEngine, SP>, ids: &[CommandId]) -> Self {
616            let mut trx = Transaction::new(GraphId::from(ids[0].into_id()));
617            let mut prior: Prior<Address> = Prior::None;
618            for &id in ids {
619                let cmd = SeqCommand::new(id, prior, 0);
620                trx.add_commands(
621                    &[cmd],
622                    &mut client.provider,
623                    &mut client.engine,
624                    &mut NullSink,
625                    &mut PeerCache::new(),
626                )
627                .unwrap();
628                prior = Prior::Single(Address { id, max_cut: 0 });
629            }
630            Self { client, trx }
631        }
632
633        pub fn line(&mut self, mut prev: Address, ids: &[CommandId]) {
634            for &id in ids {
635                let max_cut = prev.max_cut.checked_add(1).unwrap();
636                let cmd = SeqCommand::new(id, Prior::Single(prev), max_cut);
637                self.trx
638                    .add_commands(
639                        &[cmd],
640                        &mut self.client.provider,
641                        &mut self.client.engine,
642                        &mut NullSink,
643                        &mut PeerCache::new(),
644                    )
645                    .unwrap();
646                prev = Address { id, max_cut };
647            }
648        }
649
650        pub fn merge(&mut self, (left, right): (Address, Address), ids: &[CommandId]) {
651            let prior = Prior::Merge(left, right);
652            let mergecmd = SeqCommand::new(ids[0], prior, prior.next_max_cut().unwrap());
653            let mut prev = Address {
654                id: mergecmd.id,
655                max_cut: mergecmd.max_cut,
656            };
657            self.trx
658                .add_commands(
659                    &[mergecmd],
660                    &mut self.client.provider,
661                    &mut self.client.engine,
662                    &mut NullSink,
663                    &mut PeerCache::new(),
664                )
665                .unwrap();
666            for &id in &ids[1..] {
667                let cmd = SeqCommand::new(
668                    id,
669                    Prior::Single(prev),
670                    prev.max_cut.checked_add(1).expect("must not overflow"),
671                );
672                prev = Address {
673                    id: cmd.id,
674                    max_cut: cmd.max_cut,
675                };
676                self.trx
677                    .add_commands(
678                        &[cmd],
679                        &mut self.client.provider,
680                        &mut self.client.engine,
681                        &mut NullSink,
682                        &mut PeerCache::new(),
683                    )
684                    .unwrap();
685            }
686        }
687
688        pub fn flush(&mut self) {
689            if let Some(p) = Option::take(&mut self.trx.perspective) {
690                self.trx.phead = None;
691                let seg = self
692                    .client
693                    .provider
694                    .get_storage(self.trx.storage_id)
695                    .unwrap()
696                    .write(p)
697                    .unwrap();
698                let head = seg.head().unwrap();
699                self.trx.heads.insert(
700                    head.address().expect("address must exist"),
701                    seg.head_location(),
702                );
703            }
704        }
705
706        pub fn commit(&mut self) {
707            self.trx
708                .commit(
709                    &mut self.client.provider,
710                    &mut self.client.engine,
711                    &mut NullSink,
712                )
713                .unwrap();
714        }
715    }
716
717    fn mkid(x: &str) -> CommandId {
718        x.parse().unwrap()
719    }
720
721    /// See tests for usage.
722    macro_rules! graph {
723        ( $client:expr ; $init:literal $($inits:literal )* ; $($rest:tt)*) => {{
724            let mut gb = GraphBuilder::init($client, &[mkid($init), $(mkid($inits)),*]);
725            graph!(@ gb, $($rest)*);
726            gb
727        }};
728        (@ $gb:ident, $prev:literal $prev_max_cut:literal < $($id:literal)+; $($rest:tt)*) => {
729            $gb.line(Address {id: mkid($prev), max_cut: $prev_max_cut}, &[$(mkid($id)),+]);
730            graph!(@ $gb, $($rest)*);
731        };
732        (@ $gb:ident, $l:literal $l_max_cut:literal $r:literal $r_max_cut:literal < $($id:literal)+; $($rest:tt)*) => {
733            $gb.merge((Address {id: mkid($l), max_cut: $l_max_cut}, Address {id: mkid($r), max_cut: $r_max_cut}), &[$(mkid($id)),+]);
734            graph!(@ $gb, $($rest)*);
735        };
736        (@ $gb:ident, commit; $($rest:tt)*) => {
737            $gb.commit();
738            graph!(@ $gb, $($rest)*);
739        };
740        (@ $gb:ident, ) => {
741            $gb.flush();
742        };
743    }
744
745    fn lookup(storage: &impl Storage, name: &str) -> Option<Box<[u8]>> {
746        use crate::Query;
747        let head = storage.get_head().unwrap();
748        let p = storage.get_fact_perspective(head).unwrap();
749        p.query(name, &Keys::default()).unwrap()
750    }
751
752    #[test]
753    fn test_simple() -> Result<(), StorageError> {
754        let mut gb = graph! {
755            ClientState::new(SeqEngine, MemStorageProvider::new());
756            "a";
757            "a" 0 < "b";
758            "a" 0 < "c";
759            "b" 1 "c" 1 < "ma";
760            "b" 1 < "d";
761            "ma" 2 "d" 2 < "mb";
762            commit;
763        };
764        let g = gb
765            .client
766            .provider
767            .get_storage("a".parse().unwrap())
768            .unwrap();
769
770        #[cfg(feature = "graphviz")]
771        crate::storage::memory::graphviz::dot(g, "simple");
772
773        assert_eq!(g.get_head().unwrap(), Location::new(5, 0));
774
775        let seq = lookup(g, "seq").unwrap();
776        let seq = std::str::from_utf8(&seq).unwrap();
777        assert_eq!(seq, "a:b:d:c");
778
779        Ok(())
780    }
781
782    #[test]
783    fn test_complex() -> Result<(), StorageError> {
784        let mut gb = graph! {
785            ClientState::new(SeqEngine, MemStorageProvider::new());
786            "a";
787            "a" 0 < "1" "2" "3";
788            "3" 3 < "4" "6" "7";
789            "3" 3 < "5" "8";
790            "6" 5 "8" 5 < "9" "aa"; commit;
791            "7" 6 < "a1" "a2";
792            "aa" 7 "a2" 8 < "a3";
793            "a3" 9 < "a6" "a4";
794            "a3" 9 < "a7" "a5";
795            "a4" 11 "a5" 11 < "a8";
796            "9" 6 < "42" "43";
797            "42" 7 < "45" "46";
798            "45" 8 < "47" "48";
799            commit;
800        };
801
802        let g = gb
803            .client
804            .provider
805            .get_storage("a".parse().unwrap())
806            .unwrap();
807
808        #[cfg(feature = "graphviz")]
809        crate::storage::memory::graphviz::dot(g, "complex");
810
811        assert_eq!(g.get_head().unwrap(), Location::new(15, 0));
812
813        let seq = lookup(g, "seq").unwrap();
814        let seq = std::str::from_utf8(&seq).unwrap();
815        assert_eq!(
816            seq,
817            "a:1:2:3:5:8:4:6:42:45:47:48:46:43:aa:7:a1:a2:a7:a6:a5:a4"
818        );
819
820        Ok(())
821    }
822
823    #[test]
824    fn test_duplicates() {
825        let mut gb = graph! {
826            ClientState::new(SeqEngine, MemStorageProvider::new());
827            "a";
828            "a" 0 < "b" "c";
829            "a" 0 < "b";
830            "b" 1 < "c";
831            "c" 2 < "d";
832            commit;
833            "a" 0 < "b";
834            "b" 1 < "c";
835            "d" 3 < "e";
836            commit;
837        };
838
839        let g = gb
840            .client
841            .provider
842            .get_storage("a".parse().unwrap())
843            .unwrap();
844
845        #[cfg(feature = "graphviz")]
846        crate::storage::memory::graphviz::dot(g, "duplicates");
847
848        assert_eq!(g.get_head().unwrap(), Location::new(2, 0));
849
850        let seq = lookup(g, "seq").unwrap();
851        let seq = std::str::from_utf8(&seq).unwrap();
852        assert_eq!(seq, "a:b:c:d:e");
853    }
854
855    #[test]
856    fn test_mid_braid_1() {
857        let mut gb = graph! {
858            ClientState::new(SeqEngine, MemStorageProvider::new());
859            "a";
860            commit;
861            "a" 0 < "b" "c" "d" "e" "f" "g";
862            "d" 3 < "h" "i" "j";
863            commit;
864        };
865
866        let g = gb
867            .client
868            .provider
869            .get_storage("a".parse().unwrap())
870            .unwrap();
871
872        #[cfg(feature = "graphviz")]
873        crate::storage::memory::graphviz::dot(g, "mid_braid_1");
874
875        assert_eq!(g.get_head().unwrap(), Location::new(3, 0));
876
877        let seq = lookup(g, "seq").unwrap();
878        let seq = std::str::from_utf8(&seq).unwrap();
879        assert_eq!(seq, "a:b:c:d:h:i:j:e:f:g");
880    }
881
882    #[test]
883    fn test_mid_braid_2() {
884        let mut gb = graph! {
885            ClientState::new(SeqEngine, MemStorageProvider::new());
886            "a";
887            commit;
888            "a" 0 < "b" "c" "d" "h" "i" "j";
889            "d" 3 < "e" "f" "g";
890            commit;
891        };
892
893        let g = gb
894            .client
895            .provider
896            .get_storage("a".parse().unwrap())
897            .unwrap();
898
899        #[cfg(feature = "graphviz")]
900        crate::storage::memory::graphviz::dot(g, "mid_braid_2");
901
902        assert_eq!(g.get_head().unwrap(), Location::new(3, 0));
903
904        let seq = lookup(g, "seq").unwrap();
905        let seq = std::str::from_utf8(&seq).unwrap();
906        assert_eq!(seq, "a:b:c:d:h:i:j:e:f:g");
907    }
908}