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