matrix_sdk_common/linked_chunk/
relational.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Implementation for a _relational linked chunk_, see
16//! [`RelationalLinkedChunk`].
17
18use std::{collections::HashMap, hash::Hash};
19
20use ruma::{OwnedEventId, OwnedRoomId};
21
22use super::{ChunkContent, ChunkIdentifierGenerator, RawChunk};
23use crate::{
24    deserialized_responses::TimelineEvent,
25    linked_chunk::{ChunkIdentifier, LinkedChunkId, OwnedLinkedChunkId, Position, Update},
26};
27
28/// A row of the [`RelationalLinkedChunk::chunks`].
29#[derive(Debug, PartialEq)]
30struct ChunkRow {
31    linked_chunk_id: OwnedLinkedChunkId,
32    previous_chunk: Option<ChunkIdentifier>,
33    chunk: ChunkIdentifier,
34    next_chunk: Option<ChunkIdentifier>,
35}
36
37/// A row of the [`RelationalLinkedChunk::items`].
38#[derive(Debug, PartialEq)]
39struct ItemRow<ItemId, Gap> {
40    linked_chunk_id: OwnedLinkedChunkId,
41    position: Position,
42    item: Either<ItemId, Gap>,
43}
44
45/// Kind of item.
46#[derive(Debug, PartialEq)]
47enum Either<Item, Gap> {
48    /// The content is an item.
49    Item(Item),
50
51    /// The content is a gap.
52    Gap(Gap),
53}
54
55/// A [`LinkedChunk`] but with a relational layout, similar to what we
56/// would have in a database.
57///
58/// This is used by memory stores. The idea is to have a data layout that is
59/// similar for memory stores and for relational database stores, to represent a
60/// [`LinkedChunk`].
61///
62/// This type is also designed to receive [`Update`]. Applying `Update`s
63/// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as
64/// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`,
65/// they have been designed for storages, like a relational database for
66/// example.
67///
68/// This type is not as performant as [`LinkedChunk`] (in terms of memory
69/// layout, CPU caches etc.). It is only designed to be used in memory stores,
70/// which are mostly used for test purposes or light usage of the SDK.
71///
72/// [`LinkedChunk`]: super::LinkedChunk
73#[derive(Debug)]
74pub struct RelationalLinkedChunk<ItemId, Item, Gap> {
75    /// Chunks.
76    chunks: Vec<ChunkRow>,
77
78    /// Items chunks.
79    items_chunks: Vec<ItemRow<ItemId, Gap>>,
80
81    /// The items' content themselves.
82    items: HashMap<OwnedLinkedChunkId, HashMap<ItemId, Item>>,
83}
84
85/// The [`IndexableItem`] trait is used to mark items that can be indexed into a
86/// [`RelationalLinkedChunk`].
87pub trait IndexableItem {
88    type ItemId: Hash + PartialEq + Eq + Clone;
89
90    /// Return the identifier of the item.
91    fn id(&self) -> Self::ItemId;
92}
93
94impl IndexableItem for TimelineEvent {
95    type ItemId = OwnedEventId;
96
97    fn id(&self) -> Self::ItemId {
98        self.event_id()
99            .expect("all events saved into a relational linked chunk must have a valid event id")
100    }
101}
102
103impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
104where
105    Item: IndexableItem<ItemId = ItemId>,
106    ItemId: Hash + PartialEq + Eq + Clone,
107{
108    /// Create a new relational linked chunk.
109    pub fn new() -> Self {
110        Self { chunks: Vec::new(), items_chunks: Vec::new(), items: HashMap::new() }
111    }
112
113    /// Removes all the chunks and items from this relational linked chunk.
114    pub fn clear(&mut self) {
115        self.chunks.clear();
116        self.items_chunks.clear();
117        self.items.clear();
118    }
119
120    /// Apply [`Update`]s. That's the only way to write data inside this
121    /// relational linked chunk.
122    pub fn apply_updates(
123        &mut self,
124        linked_chunk_id: LinkedChunkId<'_>,
125        updates: Vec<Update<Item, Gap>>,
126    ) {
127        for update in updates {
128            match update {
129                Update::NewItemsChunk { previous, new, next } => {
130                    insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
131                }
132
133                Update::NewGapChunk { previous, new, next, gap } => {
134                    insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
135                    self.items_chunks.push(ItemRow {
136                        linked_chunk_id: linked_chunk_id.to_owned(),
137                        position: Position::new(new, 0),
138                        item: Either::Gap(gap),
139                    });
140                }
141
142                Update::RemoveChunk(chunk_identifier) => {
143                    remove_chunk(&mut self.chunks, linked_chunk_id, chunk_identifier);
144
145                    let indices_to_remove = self
146                        .items_chunks
147                        .iter()
148                        .enumerate()
149                        .filter_map(
150                            |(
151                                nth,
152                                ItemRow {
153                                    linked_chunk_id: linked_chunk_id_candidate,
154                                    position,
155                                    ..
156                                },
157                            )| {
158                                (linked_chunk_id == linked_chunk_id_candidate
159                                    && position.chunk_identifier() == chunk_identifier)
160                                    .then_some(nth)
161                            },
162                        )
163                        .collect::<Vec<_>>();
164
165                    for index_to_remove in indices_to_remove.into_iter().rev() {
166                        self.items_chunks.remove(index_to_remove);
167                    }
168                }
169
170                Update::PushItems { mut at, items } => {
171                    for item in items {
172                        let item_id = item.id();
173                        self.items
174                            .entry(linked_chunk_id.to_owned())
175                            .or_default()
176                            .insert(item_id.clone(), item);
177                        self.items_chunks.push(ItemRow {
178                            linked_chunk_id: linked_chunk_id.to_owned(),
179                            position: at,
180                            item: Either::Item(item_id),
181                        });
182                        at.increment_index();
183                    }
184                }
185
186                Update::ReplaceItem { at, item } => {
187                    let existing = self
188                        .items_chunks
189                        .iter_mut()
190                        .find(|item| item.position == at)
191                        .expect("trying to replace at an unknown position");
192                    assert!(
193                        matches!(existing.item, Either::Item(..)),
194                        "trying to replace a gap with an item"
195                    );
196                    let item_id = item.id();
197                    self.items
198                        .entry(linked_chunk_id.to_owned())
199                        .or_default()
200                        .insert(item_id.clone(), item);
201                    existing.item = Either::Item(item_id);
202                }
203
204                Update::RemoveItem { at } => {
205                    let mut entry_to_remove = None;
206
207                    for (
208                        nth,
209                        ItemRow { linked_chunk_id: linked_chunk_id_candidate, position, .. },
210                    ) in self.items_chunks.iter_mut().enumerate()
211                    {
212                        // Filter by linked chunk id.
213                        if linked_chunk_id != &*linked_chunk_id_candidate {
214                            continue;
215                        }
216
217                        // Find the item to remove.
218                        if *position == at {
219                            debug_assert!(entry_to_remove.is_none(), "Found the same entry twice");
220
221                            entry_to_remove = Some(nth);
222                        }
223
224                        // Update all items that come _after_ `at` to shift their index.
225                        if position.chunk_identifier() == at.chunk_identifier()
226                            && position.index() > at.index()
227                        {
228                            position.decrement_index();
229                        }
230                    }
231
232                    self.items_chunks.remove(entry_to_remove.expect("Remove an unknown item"));
233                    // We deliberately keep the item in the items collection.
234                }
235
236                Update::DetachLastItems { at } => {
237                    let indices_to_remove = self
238                        .items_chunks
239                        .iter()
240                        .enumerate()
241                        .filter_map(
242                            |(
243                                nth,
244                                ItemRow {
245                                    linked_chunk_id: linked_chunk_id_candidate,
246                                    position,
247                                    ..
248                                },
249                            )| {
250                                (linked_chunk_id == linked_chunk_id_candidate
251                                    && position.chunk_identifier() == at.chunk_identifier()
252                                    && position.index() >= at.index())
253                                .then_some(nth)
254                            },
255                        )
256                        .collect::<Vec<_>>();
257
258                    for index_to_remove in indices_to_remove.into_iter().rev() {
259                        self.items_chunks.remove(index_to_remove);
260                    }
261                }
262
263                Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ }
264
265                Update::Clear => {
266                    self.chunks.retain(|chunk| chunk.linked_chunk_id != linked_chunk_id);
267                    self.items_chunks.retain(|chunk| chunk.linked_chunk_id != linked_chunk_id);
268                    // We deliberately leave the items intact.
269                }
270            }
271        }
272
273        fn insert_chunk(
274            chunks: &mut Vec<ChunkRow>,
275            linked_chunk_id: LinkedChunkId<'_>,
276            previous: Option<ChunkIdentifier>,
277            new: ChunkIdentifier,
278            next: Option<ChunkIdentifier>,
279        ) {
280            // Find the previous chunk, and update its next chunk.
281            if let Some(previous) = previous {
282                let entry_for_previous_chunk = chunks
283                    .iter_mut()
284                    .find(
285                        |ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
286                            linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
287                        },
288                    )
289                    .expect("Previous chunk should be present");
290
291                // Link the chunk.
292                entry_for_previous_chunk.next_chunk = Some(new);
293            }
294
295            // Find the next chunk, and update its previous chunk.
296            if let Some(next) = next {
297                let entry_for_next_chunk = chunks
298                    .iter_mut()
299                    .find(
300                        |ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
301                            linked_chunk_id == linked_chunk_id_candidate && *chunk == next
302                        },
303                    )
304                    .expect("Next chunk should be present");
305
306                // Link the chunk.
307                entry_for_next_chunk.previous_chunk = Some(new);
308            }
309
310            // Insert the chunk.
311            chunks.push(ChunkRow {
312                linked_chunk_id: linked_chunk_id.to_owned(),
313                previous_chunk: previous,
314                chunk: new,
315                next_chunk: next,
316            });
317        }
318
319        fn remove_chunk(
320            chunks: &mut Vec<ChunkRow>,
321            linked_chunk_id: LinkedChunkId<'_>,
322            chunk_to_remove: ChunkIdentifier,
323        ) {
324            let entry_nth_to_remove = chunks
325                .iter()
326                .enumerate()
327                .find_map(
328                    |(nth, ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. })| {
329                        (linked_chunk_id == linked_chunk_id_candidate && *chunk == chunk_to_remove)
330                            .then_some(nth)
331                    },
332                )
333                .expect("Remove an unknown chunk");
334
335            let ChunkRow { linked_chunk_id, previous_chunk: previous, next_chunk: next, .. } =
336                chunks.remove(entry_nth_to_remove);
337
338            // Find the previous chunk, and update its next chunk.
339            if let Some(previous) = previous {
340                let entry_for_previous_chunk = chunks
341                    .iter_mut()
342                    .find(
343                        |ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
344                            &linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
345                        },
346                    )
347                    .expect("Previous chunk should be present");
348
349                // Insert the chunk.
350                entry_for_previous_chunk.next_chunk = next;
351            }
352
353            // Find the next chunk, and update its previous chunk.
354            if let Some(next) = next {
355                let entry_for_next_chunk = chunks
356                    .iter_mut()
357                    .find(
358                        |ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
359                            &linked_chunk_id == linked_chunk_id_candidate && *chunk == next
360                        },
361                    )
362                    .expect("Next chunk should be present");
363
364                // Insert the chunk.
365                entry_for_next_chunk.previous_chunk = previous;
366            }
367        }
368    }
369
370    /// Return an iterator that yields items of a particular linked chunk, in no
371    /// particular order.
372    pub fn unordered_linked_chunk_items<'a>(
373        &'a self,
374        target: LinkedChunkId<'a>,
375    ) -> impl Iterator<Item = (&'a Item, Position)> {
376        self.items_chunks.iter().filter_map(move |item_row| {
377            if item_row.linked_chunk_id == target {
378                match &item_row.item {
379                    Either::Item(item_id) => {
380                        Some((self.items.get(&target.to_owned())?.get(item_id)?, item_row.position))
381                    }
382                    Either::Gap(..) => None,
383                }
384            } else {
385                None
386            }
387        })
388    }
389
390    /// Return an iterator over all items of all room linked chunks, without
391    /// their actual positions.
392    ///
393    /// This will include out-of-band items.
394    pub fn items(&self) -> impl Iterator<Item = (&Item, LinkedChunkId<'_>)> {
395        self.items.iter().flat_map(|(linked_chunk_id, items)| {
396            items.values().map(|item| (item, linked_chunk_id.as_ref()))
397        })
398    }
399
400    /// Save a single item "out-of-band" in the relational linked chunk.
401    pub fn save_item(&mut self, room_id: OwnedRoomId, item: Item) {
402        let id = item.id();
403        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id);
404        self.items.entry(linked_chunk_id).or_default().insert(id, item);
405    }
406}
407
408impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
409where
410    Gap: Clone,
411    Item: Clone,
412    ItemId: Hash + PartialEq + Eq,
413{
414    /// Loads all the chunks.
415    ///
416    /// Return an error result if the data was malformed in the struct, with a
417    /// string message explaining details about the error.
418    #[doc(hidden)]
419    pub fn load_all_chunks(
420        &self,
421        linked_chunk_id: LinkedChunkId<'_>,
422    ) -> Result<Vec<RawChunk<Item, Gap>>, String> {
423        self.chunks
424            .iter()
425            .filter(|chunk| chunk.linked_chunk_id == linked_chunk_id)
426            .map(|chunk_row| load_raw_chunk(self, chunk_row, linked_chunk_id))
427            .collect::<Result<Vec<_>, String>>()
428    }
429
430    pub fn load_last_chunk(
431        &self,
432        linked_chunk_id: LinkedChunkId<'_>,
433    ) -> Result<(Option<RawChunk<Item, Gap>>, ChunkIdentifierGenerator), String> {
434        // Find the latest chunk identifier to generate a `ChunkIdentifierGenerator`.
435        let chunk_identifier_generator = match self
436            .chunks
437            .iter()
438            .filter_map(|chunk_row| {
439                (chunk_row.linked_chunk_id == linked_chunk_id).then_some(chunk_row.chunk)
440            })
441            .max()
442        {
443            Some(last_chunk_identifier) => {
444                ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier)
445            }
446            None => ChunkIdentifierGenerator::new_from_scratch(),
447        };
448
449        // Find the last chunk.
450        let mut number_of_chunks = 0;
451        let mut chunk_row = None;
452
453        for chunk_row_candidate in &self.chunks {
454            if chunk_row_candidate.linked_chunk_id == linked_chunk_id {
455                number_of_chunks += 1;
456
457                if chunk_row_candidate.next_chunk.is_none() {
458                    chunk_row = Some(chunk_row_candidate);
459
460                    break;
461                }
462            }
463        }
464
465        let chunk_row = match chunk_row {
466            // Chunk has been found, all good.
467            Some(chunk_row) => chunk_row,
468
469            // Chunk is not found and there is zero chunk for this room, this is consistent, all
470            // good.
471            None if number_of_chunks == 0 => {
472                return Ok((None, chunk_identifier_generator));
473            }
474
475            // Chunk is not found **but** there are chunks for this room, this is inconsistent. The
476            // linked chunk is malformed.
477            //
478            // Returning `Ok(None)` would be invalid here: we must return an error.
479            None => {
480                return Err(
481                    "last chunk is not found but chunks exist: the linked chunk contains a cycle"
482                        .to_owned(),
483                );
484            }
485        };
486
487        // Build the chunk.
488        load_raw_chunk(self, chunk_row, linked_chunk_id)
489            .map(|raw_chunk| (Some(raw_chunk), chunk_identifier_generator))
490    }
491
492    pub fn load_previous_chunk(
493        &self,
494        linked_chunk_id: LinkedChunkId<'_>,
495        before_chunk_identifier: ChunkIdentifier,
496    ) -> Result<Option<RawChunk<Item, Gap>>, String> {
497        // Find the chunk before the chunk identified by `before_chunk_identifier`.
498        let Some(chunk_row) = self.chunks.iter().find(|chunk_row| {
499            chunk_row.linked_chunk_id == linked_chunk_id
500                && chunk_row.next_chunk == Some(before_chunk_identifier)
501        }) else {
502            // Chunk is not found.
503            return Ok(None);
504        };
505
506        // Build the chunk.
507        load_raw_chunk(self, chunk_row, linked_chunk_id).map(Some)
508    }
509}
510
511impl<ItemId, Item, Gap> Default for RelationalLinkedChunk<ItemId, Item, Gap>
512where
513    Item: IndexableItem<ItemId = ItemId>,
514    ItemId: Hash + PartialEq + Eq + Clone,
515{
516    fn default() -> Self {
517        Self::new()
518    }
519}
520
521fn load_raw_chunk<ItemId, Item, Gap>(
522    relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
523    chunk_row: &ChunkRow,
524    linked_chunk_id: LinkedChunkId<'_>,
525) -> Result<RawChunk<Item, Gap>, String>
526where
527    Item: Clone,
528    Gap: Clone,
529    ItemId: Hash + PartialEq + Eq,
530{
531    // Find all items that correspond to the chunk.
532    let mut items = relational_linked_chunk
533        .items_chunks
534        .iter()
535        .filter(|item_row| {
536            item_row.linked_chunk_id == linked_chunk_id
537                && item_row.position.chunk_identifier() == chunk_row.chunk
538        })
539        .peekable();
540
541    let Some(first_item) = items.peek() else {
542        // No item. It means it is a chunk of kind `Items` and that it is empty!
543        return Ok(RawChunk {
544            content: ChunkContent::Items(Vec::new()),
545            previous: chunk_row.previous_chunk,
546            identifier: chunk_row.chunk,
547            next: chunk_row.next_chunk,
548        });
549    };
550
551    Ok(match first_item.item {
552        // This is a chunk of kind `Items`.
553        Either::Item(_) => {
554            // Collect all the items.
555            let mut collected_items = Vec::new();
556
557            for item_row in items {
558                match &item_row.item {
559                    Either::Item(item_id) => {
560                        collected_items.push((item_id, item_row.position.index()))
561                    }
562
563                    Either::Gap(_) => {
564                        return Err(format!(
565                            "unexpected gap in items chunk {}",
566                            chunk_row.chunk.index()
567                        ));
568                    }
569                }
570            }
571
572            // Sort them by their position.
573            collected_items.sort_unstable_by_key(|(_item, index)| *index);
574
575            RawChunk {
576                content: ChunkContent::Items(
577                    collected_items
578                        .into_iter()
579                        .filter_map(|(item_id, _index)| {
580                            relational_linked_chunk
581                                .items
582                                .get(&linked_chunk_id.to_owned())?
583                                .get(item_id)
584                                .cloned()
585                        })
586                        .collect(),
587                ),
588                previous: chunk_row.previous_chunk,
589                identifier: chunk_row.chunk,
590                next: chunk_row.next_chunk,
591            }
592        }
593
594        Either::Gap(ref gap) => {
595            assert!(items.next().is_some(), "we just peeked the gap");
596
597            // We shouldn't have more than one item row for this chunk.
598            if items.next().is_some() {
599                return Err(format!(
600                    "there shouldn't be more than one item row attached in gap chunk {}",
601                    chunk_row.chunk.index()
602                ));
603            }
604
605            RawChunk {
606                content: ChunkContent::Gap(gap.clone()),
607                previous: chunk_row.previous_chunk,
608                identifier: chunk_row.chunk,
609                next: chunk_row.next_chunk,
610            }
611        }
612    })
613}
614
615#[cfg(test)]
616mod tests {
617    use assert_matches::assert_matches;
618    use ruma::room_id;
619
620    use super::{super::lazy_loader::from_all_chunks, ChunkIdentifier as CId, *};
621
622    impl IndexableItem for char {
623        type ItemId = char;
624
625        fn id(&self) -> Self::ItemId {
626            *self
627        }
628    }
629
630    #[test]
631    fn test_new_items_chunk() {
632        let room_id = room_id!("!r0:matrix.org");
633        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
634
635        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
636
637        relational_linked_chunk.apply_updates(
638            linked_chunk_id.as_ref(),
639            vec![
640                // 0
641                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
642                // 1 after 0
643                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
644                // 2 before 0
645                Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
646                // 3 between 2 and 0
647                Update::NewItemsChunk {
648                    previous: Some(CId::new(2)),
649                    new: CId::new(3),
650                    next: Some(CId::new(0)),
651                },
652            ],
653        );
654
655        // Chunks are correctly linked.
656        assert_eq!(
657            relational_linked_chunk.chunks,
658            &[
659                ChunkRow {
660                    linked_chunk_id: linked_chunk_id.clone(),
661                    previous_chunk: Some(CId::new(3)),
662                    chunk: CId::new(0),
663                    next_chunk: Some(CId::new(1))
664                },
665                ChunkRow {
666                    linked_chunk_id: linked_chunk_id.clone(),
667                    previous_chunk: Some(CId::new(0)),
668                    chunk: CId::new(1),
669                    next_chunk: None
670                },
671                ChunkRow {
672                    linked_chunk_id: linked_chunk_id.clone(),
673                    previous_chunk: None,
674                    chunk: CId::new(2),
675                    next_chunk: Some(CId::new(3))
676                },
677                ChunkRow {
678                    linked_chunk_id,
679                    previous_chunk: Some(CId::new(2)),
680                    chunk: CId::new(3),
681                    next_chunk: Some(CId::new(0))
682                },
683            ],
684        );
685
686        // Items have not been modified.
687        assert!(relational_linked_chunk.items_chunks.is_empty());
688    }
689
690    #[test]
691    fn test_new_gap_chunk() {
692        let room_id = room_id!("!r0:matrix.org");
693        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
694
695        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
696
697        relational_linked_chunk.apply_updates(
698            linked_chunk_id.as_ref(),
699            vec![
700                // 0
701                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
702                // 1 after 0
703                Update::NewGapChunk {
704                    previous: Some(CId::new(0)),
705                    new: CId::new(1),
706                    next: None,
707                    gap: (),
708                },
709                // 2 after 1
710                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
711            ],
712        );
713
714        // Chunks are correctly linked.
715        assert_eq!(
716            relational_linked_chunk.chunks,
717            &[
718                ChunkRow {
719                    linked_chunk_id: linked_chunk_id.clone(),
720                    previous_chunk: None,
721                    chunk: CId::new(0),
722                    next_chunk: Some(CId::new(1))
723                },
724                ChunkRow {
725                    linked_chunk_id: linked_chunk_id.clone(),
726                    previous_chunk: Some(CId::new(0)),
727                    chunk: CId::new(1),
728                    next_chunk: Some(CId::new(2))
729                },
730                ChunkRow {
731                    linked_chunk_id: linked_chunk_id.clone(),
732                    previous_chunk: Some(CId::new(1)),
733                    chunk: CId::new(2),
734                    next_chunk: None
735                },
736            ],
737        );
738        // Items contains the gap.
739        assert_eq!(
740            relational_linked_chunk.items_chunks,
741            &[ItemRow {
742                linked_chunk_id,
743                position: Position::new(CId::new(1), 0),
744                item: Either::Gap(())
745            }],
746        );
747    }
748
749    #[test]
750    fn test_remove_chunk() {
751        let room_id = room_id!("!r0:matrix.org");
752        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
753
754        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
755
756        relational_linked_chunk.apply_updates(
757            linked_chunk_id.as_ref(),
758            vec![
759                // 0
760                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
761                // 1 after 0
762                Update::NewGapChunk {
763                    previous: Some(CId::new(0)),
764                    new: CId::new(1),
765                    next: None,
766                    gap: (),
767                },
768                // 2 after 1
769                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
770                // remove 1
771                Update::RemoveChunk(CId::new(1)),
772            ],
773        );
774
775        // Chunks are correctly linked.
776        assert_eq!(
777            relational_linked_chunk.chunks,
778            &[
779                ChunkRow {
780                    linked_chunk_id: linked_chunk_id.clone(),
781                    previous_chunk: None,
782                    chunk: CId::new(0),
783                    next_chunk: Some(CId::new(2))
784                },
785                ChunkRow {
786                    linked_chunk_id,
787                    previous_chunk: Some(CId::new(0)),
788                    chunk: CId::new(2),
789                    next_chunk: None
790                },
791            ],
792        );
793
794        // Items no longer contains the gap.
795        assert!(relational_linked_chunk.items_chunks.is_empty());
796    }
797
798    #[test]
799    fn test_push_items() {
800        let room_id = room_id!("!r0:matrix.org");
801        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
802
803        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
804
805        relational_linked_chunk.apply_updates(
806            linked_chunk_id.as_ref(),
807            vec![
808                // new chunk (this is not mandatory for this test, but let's try to be realistic)
809                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
810                // new items on 0
811                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
812                // new chunk (to test new items are pushed in the correct chunk)
813                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
814                // new items on 1
815                Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
816                // new items on 0 again
817                Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
818            ],
819        );
820
821        // Chunks are correctly linked.
822        assert_eq!(
823            relational_linked_chunk.chunks,
824            &[
825                ChunkRow {
826                    linked_chunk_id: linked_chunk_id.clone(),
827                    previous_chunk: None,
828                    chunk: CId::new(0),
829                    next_chunk: Some(CId::new(1))
830                },
831                ChunkRow {
832                    linked_chunk_id: linked_chunk_id.clone(),
833                    previous_chunk: Some(CId::new(0)),
834                    chunk: CId::new(1),
835                    next_chunk: None
836                },
837            ],
838        );
839        // Items contains the pushed items.
840        assert_eq!(
841            relational_linked_chunk.items_chunks,
842            &[
843                ItemRow {
844                    linked_chunk_id: linked_chunk_id.clone(),
845                    position: Position::new(CId::new(0), 0),
846                    item: Either::Item('a')
847                },
848                ItemRow {
849                    linked_chunk_id: linked_chunk_id.clone(),
850                    position: Position::new(CId::new(0), 1),
851                    item: Either::Item('b')
852                },
853                ItemRow {
854                    linked_chunk_id: linked_chunk_id.clone(),
855                    position: Position::new(CId::new(0), 2),
856                    item: Either::Item('c')
857                },
858                ItemRow {
859                    linked_chunk_id: linked_chunk_id.clone(),
860                    position: Position::new(CId::new(1), 0),
861                    item: Either::Item('x')
862                },
863                ItemRow {
864                    linked_chunk_id: linked_chunk_id.clone(),
865                    position: Position::new(CId::new(1), 1),
866                    item: Either::Item('y')
867                },
868                ItemRow {
869                    linked_chunk_id: linked_chunk_id.clone(),
870                    position: Position::new(CId::new(1), 2),
871                    item: Either::Item('z')
872                },
873                ItemRow {
874                    linked_chunk_id: linked_chunk_id.clone(),
875                    position: Position::new(CId::new(0), 3),
876                    item: Either::Item('d')
877                },
878                ItemRow {
879                    linked_chunk_id: linked_chunk_id.clone(),
880                    position: Position::new(CId::new(0), 4),
881                    item: Either::Item('e')
882                },
883            ],
884        );
885    }
886
887    #[test]
888    fn test_remove_item() {
889        let room_id = room_id!("!r0:matrix.org");
890        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
891
892        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
893
894        relational_linked_chunk.apply_updates(
895            linked_chunk_id.as_ref(),
896            vec![
897                // new chunk (this is not mandatory for this test, but let's try to be realistic)
898                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
899                // new items on 0
900                Update::PushItems {
901                    at: Position::new(CId::new(0), 0),
902                    items: vec!['a', 'b', 'c', 'd', 'e'],
903                },
904                // remove an item: 'a'
905                Update::RemoveItem { at: Position::new(CId::new(0), 0) },
906                // remove an item: 'd'
907                Update::RemoveItem { at: Position::new(CId::new(0), 2) },
908            ],
909        );
910
911        // Chunks are correctly linked.
912        assert_eq!(
913            relational_linked_chunk.chunks,
914            &[ChunkRow {
915                linked_chunk_id: linked_chunk_id.clone(),
916                previous_chunk: None,
917                chunk: CId::new(0),
918                next_chunk: None
919            }],
920        );
921        // Items contains the pushed items.
922        assert_eq!(
923            relational_linked_chunk.items_chunks,
924            &[
925                ItemRow {
926                    linked_chunk_id: linked_chunk_id.clone(),
927                    position: Position::new(CId::new(0), 0),
928                    item: Either::Item('b')
929                },
930                ItemRow {
931                    linked_chunk_id: linked_chunk_id.clone(),
932                    position: Position::new(CId::new(0), 1),
933                    item: Either::Item('c')
934                },
935                ItemRow {
936                    linked_chunk_id: linked_chunk_id.clone(),
937                    position: Position::new(CId::new(0), 2),
938                    item: Either::Item('e')
939                },
940            ],
941        );
942    }
943
944    #[test]
945    fn test_detach_last_items() {
946        let room_id = room_id!("!r0:matrix.org");
947        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
948
949        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
950
951        relational_linked_chunk.apply_updates(
952            linked_chunk_id.as_ref(),
953            vec![
954                // new chunk
955                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
956                // new chunk
957                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
958                // new items on 0
959                Update::PushItems {
960                    at: Position::new(CId::new(0), 0),
961                    items: vec!['a', 'b', 'c', 'd', 'e'],
962                },
963                // new items on 1
964                Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
965                // detach last items on 0
966                Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
967            ],
968        );
969
970        // Chunks are correctly linked.
971        assert_eq!(
972            relational_linked_chunk.chunks,
973            &[
974                ChunkRow {
975                    linked_chunk_id: linked_chunk_id.clone(),
976                    previous_chunk: None,
977                    chunk: CId::new(0),
978                    next_chunk: Some(CId::new(1))
979                },
980                ChunkRow {
981                    linked_chunk_id: linked_chunk_id.clone(),
982                    previous_chunk: Some(CId::new(0)),
983                    chunk: CId::new(1),
984                    next_chunk: None
985                },
986            ],
987        );
988        // Items contains the pushed items.
989        assert_eq!(
990            relational_linked_chunk.items_chunks,
991            &[
992                ItemRow {
993                    linked_chunk_id: linked_chunk_id.clone(),
994                    position: Position::new(CId::new(0), 0),
995                    item: Either::Item('a')
996                },
997                ItemRow {
998                    linked_chunk_id: linked_chunk_id.clone(),
999                    position: Position::new(CId::new(0), 1),
1000                    item: Either::Item('b')
1001                },
1002                ItemRow {
1003                    linked_chunk_id: linked_chunk_id.clone(),
1004                    position: Position::new(CId::new(1), 0),
1005                    item: Either::Item('x')
1006                },
1007                ItemRow {
1008                    linked_chunk_id: linked_chunk_id.clone(),
1009                    position: Position::new(CId::new(1), 1),
1010                    item: Either::Item('y')
1011                },
1012                ItemRow {
1013                    linked_chunk_id: linked_chunk_id.clone(),
1014                    position: Position::new(CId::new(1), 2),
1015                    item: Either::Item('z')
1016                },
1017            ],
1018        );
1019    }
1020
1021    #[test]
1022    fn test_start_and_end_reattach_items() {
1023        let room_id = room_id!("!r0:matrix.org");
1024        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1025
1026        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1027
1028        relational_linked_chunk.apply_updates(
1029            linked_chunk_id.as_ref(),
1030            vec![Update::StartReattachItems, Update::EndReattachItems],
1031        );
1032
1033        // Nothing happened.
1034        assert!(relational_linked_chunk.chunks.is_empty());
1035        assert!(relational_linked_chunk.items_chunks.is_empty());
1036    }
1037
1038    #[test]
1039    fn test_clear() {
1040        let r0 = room_id!("!r0:matrix.org");
1041        let linked_chunk_id0 = OwnedLinkedChunkId::Room(r0.to_owned());
1042
1043        let r1 = room_id!("!r1:matrix.org");
1044        let linked_chunk_id1 = OwnedLinkedChunkId::Room(r1.to_owned());
1045
1046        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1047
1048        relational_linked_chunk.apply_updates(
1049            linked_chunk_id0.as_ref(),
1050            vec![
1051                // new chunk (this is not mandatory for this test, but let's try to be realistic)
1052                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1053                // new items on 0
1054                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
1055            ],
1056        );
1057
1058        relational_linked_chunk.apply_updates(
1059            linked_chunk_id1.as_ref(),
1060            vec![
1061                // new chunk (this is not mandatory for this test, but let's try to be realistic)
1062                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1063                // new items on 0
1064                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x'] },
1065            ],
1066        );
1067
1068        // Chunks are correctly linked.
1069        assert_eq!(
1070            relational_linked_chunk.chunks,
1071            &[
1072                ChunkRow {
1073                    linked_chunk_id: linked_chunk_id0.to_owned(),
1074                    previous_chunk: None,
1075                    chunk: CId::new(0),
1076                    next_chunk: None,
1077                },
1078                ChunkRow {
1079                    linked_chunk_id: linked_chunk_id1.to_owned(),
1080                    previous_chunk: None,
1081                    chunk: CId::new(0),
1082                    next_chunk: None,
1083                }
1084            ],
1085        );
1086
1087        // Items contains the pushed items.
1088        assert_eq!(
1089            relational_linked_chunk.items_chunks,
1090            &[
1091                ItemRow {
1092                    linked_chunk_id: linked_chunk_id0.to_owned(),
1093                    position: Position::new(CId::new(0), 0),
1094                    item: Either::Item('a')
1095                },
1096                ItemRow {
1097                    linked_chunk_id: linked_chunk_id0.to_owned(),
1098                    position: Position::new(CId::new(0), 1),
1099                    item: Either::Item('b')
1100                },
1101                ItemRow {
1102                    linked_chunk_id: linked_chunk_id0.to_owned(),
1103                    position: Position::new(CId::new(0), 2),
1104                    item: Either::Item('c')
1105                },
1106                ItemRow {
1107                    linked_chunk_id: linked_chunk_id1.to_owned(),
1108                    position: Position::new(CId::new(0), 0),
1109                    item: Either::Item('x')
1110                },
1111            ],
1112        );
1113
1114        // Now, time for a clean up.
1115        relational_linked_chunk.apply_updates(linked_chunk_id0.as_ref(), vec![Update::Clear]);
1116
1117        // Only items from r1 remain.
1118        assert_eq!(
1119            relational_linked_chunk.chunks,
1120            &[ChunkRow {
1121                linked_chunk_id: linked_chunk_id1.to_owned(),
1122                previous_chunk: None,
1123                chunk: CId::new(0),
1124                next_chunk: None,
1125            }],
1126        );
1127
1128        assert_eq!(
1129            relational_linked_chunk.items_chunks,
1130            &[ItemRow {
1131                linked_chunk_id: linked_chunk_id1.to_owned(),
1132                position: Position::new(CId::new(0), 0),
1133                item: Either::Item('x')
1134            },],
1135        );
1136    }
1137
1138    #[test]
1139    fn test_load_empty_linked_chunk() {
1140        let room_id = room_id!("!r0:matrix.org");
1141        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1142
1143        // When I reload the linked chunk components from an empty store,
1144        let relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
1145        let result = relational_linked_chunk.load_all_chunks(linked_chunk_id.as_ref()).unwrap();
1146        assert!(result.is_empty());
1147    }
1148
1149    #[test]
1150    fn test_load_all_chunks_with_empty_items() {
1151        let room_id = room_id!("!r0:matrix.org");
1152        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1153
1154        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
1155
1156        // When I store an empty items chunks,
1157        relational_linked_chunk.apply_updates(
1158            linked_chunk_id.as_ref(),
1159            vec![Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }],
1160        );
1161
1162        // It correctly gets reloaded as such.
1163        let lc = from_all_chunks::<3, _, _>(
1164            relational_linked_chunk.load_all_chunks(linked_chunk_id.as_ref()).unwrap(),
1165        )
1166        .expect("building succeeds")
1167        .expect("this leads to a non-empty linked chunk");
1168
1169        assert_items_eq!(lc, []);
1170    }
1171
1172    #[test]
1173    fn test_rebuild_linked_chunk() {
1174        let room_id = room_id!("!r0:matrix.org");
1175        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1176
1177        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
1178
1179        relational_linked_chunk.apply_updates(
1180            linked_chunk_id.as_ref(),
1181            vec![
1182                // new chunk
1183                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1184                // new items on 0
1185                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
1186                // a gap chunk
1187                Update::NewGapChunk {
1188                    previous: Some(CId::new(0)),
1189                    new: CId::new(1),
1190                    next: None,
1191                    gap: 'g',
1192                },
1193                // another items chunk
1194                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
1195                // new items on 0
1196                Update::PushItems { at: Position::new(CId::new(2), 0), items: vec!['d', 'e', 'f'] },
1197            ],
1198        );
1199
1200        let lc = from_all_chunks::<3, _, _>(
1201            relational_linked_chunk.load_all_chunks(linked_chunk_id.as_ref()).unwrap(),
1202        )
1203        .expect("building succeeds")
1204        .expect("this leads to a non-empty linked chunk");
1205
1206        // The linked chunk is correctly reloaded.
1207        assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
1208    }
1209
1210    #[test]
1211    fn test_replace_item() {
1212        let room_id = room_id!("!r0:matrix.org");
1213        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1214
1215        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1216
1217        relational_linked_chunk.apply_updates(
1218            linked_chunk_id.as_ref(),
1219            vec![
1220                // new chunk (this is not mandatory for this test, but let's try to be realistic)
1221                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1222                // new items on 0
1223                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
1224                // update item at (0; 1).
1225                Update::ReplaceItem { at: Position::new(CId::new(0), 1), item: 'B' },
1226            ],
1227        );
1228
1229        // Chunks are correctly linked.
1230        assert_eq!(
1231            relational_linked_chunk.chunks,
1232            &[ChunkRow {
1233                linked_chunk_id: linked_chunk_id.clone(),
1234                previous_chunk: None,
1235                chunk: CId::new(0),
1236                next_chunk: None,
1237            },],
1238        );
1239
1240        // Items contains the pushed *and* replaced items.
1241        assert_eq!(
1242            relational_linked_chunk.items_chunks,
1243            &[
1244                ItemRow {
1245                    linked_chunk_id: linked_chunk_id.clone(),
1246                    position: Position::new(CId::new(0), 0),
1247                    item: Either::Item('a')
1248                },
1249                ItemRow {
1250                    linked_chunk_id: linked_chunk_id.clone(),
1251                    position: Position::new(CId::new(0), 1),
1252                    item: Either::Item('B')
1253                },
1254                ItemRow {
1255                    linked_chunk_id,
1256                    position: Position::new(CId::new(0), 2),
1257                    item: Either::Item('c')
1258                },
1259            ],
1260        );
1261    }
1262
1263    #[test]
1264    fn test_unordered_events() {
1265        let room_id = room_id!("!r0:matrix.org");
1266        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1267
1268        let other_room_id = room_id!("!r1:matrix.org");
1269        let other_linked_chunk_id = OwnedLinkedChunkId::Room(other_room_id.to_owned());
1270
1271        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1272
1273        relational_linked_chunk.apply_updates(
1274            linked_chunk_id.as_ref(),
1275            vec![
1276                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1277                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
1278                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
1279                Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['d', 'e', 'f'] },
1280            ],
1281        );
1282
1283        relational_linked_chunk.apply_updates(
1284            other_linked_chunk_id.as_ref(),
1285            vec![
1286                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1287                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x', 'y', 'z'] },
1288            ],
1289        );
1290
1291        let mut events =
1292            relational_linked_chunk.unordered_linked_chunk_items(linked_chunk_id.as_ref());
1293
1294        assert_eq!(events.next().unwrap(), (&'a', Position::new(CId::new(0), 0)));
1295        assert_eq!(events.next().unwrap(), (&'b', Position::new(CId::new(0), 1)));
1296        assert_eq!(events.next().unwrap(), (&'c', Position::new(CId::new(0), 2)));
1297        assert_eq!(events.next().unwrap(), (&'d', Position::new(CId::new(1), 0)));
1298        assert_eq!(events.next().unwrap(), (&'e', Position::new(CId::new(1), 1)));
1299        assert_eq!(events.next().unwrap(), (&'f', Position::new(CId::new(1), 2)));
1300        assert!(events.next().is_none());
1301    }
1302
1303    #[test]
1304    fn test_load_last_chunk() {
1305        let room_id = room_id!("!r0:matrix.org");
1306        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1307
1308        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1309
1310        // Case #1: no last chunk.
1311        {
1312            let (last_chunk, chunk_identifier_generator) =
1313                relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap();
1314
1315            assert!(last_chunk.is_none());
1316            assert_eq!(chunk_identifier_generator.current(), 0);
1317        }
1318
1319        // Case #2: only one chunk is present.
1320        {
1321            relational_linked_chunk.apply_updates(
1322                linked_chunk_id.as_ref(),
1323                vec![
1324                    Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
1325                    Update::PushItems { at: Position::new(CId::new(42), 0), items: vec!['a', 'b'] },
1326                ],
1327            );
1328
1329            let (last_chunk, chunk_identifier_generator) =
1330                relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap();
1331
1332            assert_matches!(last_chunk, Some(last_chunk) => {
1333                assert_eq!(last_chunk.identifier, 42);
1334                assert!(last_chunk.previous.is_none());
1335                assert!(last_chunk.next.is_none());
1336                assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
1337                    assert_eq!(items.len(), 2);
1338                    assert_eq!(items, &['a', 'b']);
1339                });
1340            });
1341            assert_eq!(chunk_identifier_generator.current(), 42);
1342        }
1343
1344        // Case #3: more chunks are present.
1345        {
1346            relational_linked_chunk.apply_updates(
1347                linked_chunk_id.as_ref(),
1348                vec![
1349                    Update::NewItemsChunk {
1350                        previous: Some(CId::new(42)),
1351                        new: CId::new(7),
1352                        next: None,
1353                    },
1354                    Update::PushItems {
1355                        at: Position::new(CId::new(7), 0),
1356                        items: vec!['c', 'd', 'e'],
1357                    },
1358                ],
1359            );
1360
1361            let (last_chunk, chunk_identifier_generator) =
1362                relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap();
1363
1364            assert_matches!(last_chunk, Some(last_chunk) => {
1365                assert_eq!(last_chunk.identifier, 7);
1366                assert_matches!(last_chunk.previous, Some(previous) => {
1367                    assert_eq!(previous, 42);
1368                });
1369                assert!(last_chunk.next.is_none());
1370                assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
1371                    assert_eq!(items.len(), 3);
1372                    assert_eq!(items, &['c', 'd', 'e']);
1373                });
1374            });
1375            assert_eq!(chunk_identifier_generator.current(), 42);
1376        }
1377    }
1378
1379    #[test]
1380    fn test_load_last_chunk_with_a_cycle() {
1381        let room_id = room_id!("!r0:matrix.org");
1382        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1383        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1384
1385        relational_linked_chunk.apply_updates(
1386            linked_chunk_id.as_ref(),
1387            vec![
1388                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
1389                Update::NewItemsChunk {
1390                    // Because `previous` connects to chunk #0, it will create a cycle.
1391                    // Chunk #0 will have a `next` set to chunk #1! Consequently, the last chunk
1392                    // **does not exist**. We have to detect this cycle.
1393                    previous: Some(CId::new(0)),
1394                    new: CId::new(1),
1395                    next: Some(CId::new(0)),
1396                },
1397            ],
1398        );
1399
1400        relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap_err();
1401    }
1402
1403    #[test]
1404    fn test_load_previous_chunk() {
1405        let room_id = room_id!("!r0:matrix.org");
1406        let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
1407        let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
1408
1409        // Case #1: no chunk at all, equivalent to having an inexistent
1410        // `before_chunk_identifier`.
1411        {
1412            let previous_chunk = relational_linked_chunk
1413                .load_previous_chunk(linked_chunk_id.as_ref(), CId::new(153))
1414                .unwrap();
1415
1416            assert!(previous_chunk.is_none());
1417        }
1418
1419        // Case #2: there is one chunk only: we request the previous on this
1420        // one, it doesn't exist.
1421        {
1422            relational_linked_chunk.apply_updates(
1423                linked_chunk_id.as_ref(),
1424                vec![Update::NewItemsChunk { previous: None, new: CId::new(42), next: None }],
1425            );
1426
1427            let previous_chunk = relational_linked_chunk
1428                .load_previous_chunk(linked_chunk_id.as_ref(), CId::new(42))
1429                .unwrap();
1430
1431            assert!(previous_chunk.is_none());
1432        }
1433
1434        // Case #3: there is two chunks.
1435        {
1436            relational_linked_chunk.apply_updates(
1437                linked_chunk_id.as_ref(),
1438                vec![
1439                    // new chunk before the one that exists.
1440                    Update::NewItemsChunk {
1441                        previous: None,
1442                        new: CId::new(7),
1443                        next: Some(CId::new(42)),
1444                    },
1445                    Update::PushItems {
1446                        at: Position::new(CId::new(7), 0),
1447                        items: vec!['a', 'b', 'c'],
1448                    },
1449                ],
1450            );
1451
1452            let previous_chunk = relational_linked_chunk
1453                .load_previous_chunk(linked_chunk_id.as_ref(), CId::new(42))
1454                .unwrap();
1455
1456            assert_matches!(previous_chunk, Some(previous_chunk) => {
1457                assert_eq!(previous_chunk.identifier, 7);
1458                assert!(previous_chunk.previous.is_none());
1459                assert_matches!(previous_chunk.next, Some(next) => {
1460                    assert_eq!(next, 42);
1461                });
1462                assert_matches!(previous_chunk.content, ChunkContent::Items(items) => {
1463                    assert_eq!(items.len(), 3);
1464                    assert_eq!(items, &['a', 'b', 'c']);
1465                });
1466            });
1467        }
1468    }
1469}