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