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