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