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, 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
297impl<Item, Gap> RelationalLinkedChunk<Item, Gap>
298where
299    Gap: Clone,
300    Item: Clone,
301{
302    /// Reloads the chunks.
303    ///
304    /// Return an error result if the data was malformed in the struct, with a
305    /// string message explaining details about the error.
306    pub fn reload_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
307        let mut result = Vec::new();
308
309        for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) {
310            // Find all items that correspond to the chunk.
311            let mut items = self
312                .items
313                .iter()
314                .filter(|row| {
315                    row.room_id == room_id && row.position.chunk_identifier() == chunk_row.chunk
316                })
317                .peekable();
318
319            // Look at the first chunk item type, to reconstruct the chunk at hand.
320            let Some(first) = items.peek() else {
321                // The only possibility is that we created an empty items chunk; mark it as
322                // such, and continue.
323                result.push(RawChunk {
324                    content: ChunkContent::Items(Vec::new()),
325                    previous: chunk_row.previous_chunk,
326                    identifier: chunk_row.chunk,
327                    next: chunk_row.next_chunk,
328                });
329                continue;
330            };
331
332            match &first.item {
333                Either::Item(_) => {
334                    // Collect all the related items.
335                    let mut collected_items = Vec::new();
336                    for row in items {
337                        match &row.item {
338                            Either::Item(item) => {
339                                collected_items.push((item.clone(), row.position.index()))
340                            }
341                            Either::Gap(_) => {
342                                return Err(format!(
343                                    "unexpected gap in items chunk {}",
344                                    chunk_row.chunk.index()
345                                ));
346                            }
347                        }
348                    }
349
350                    // Sort them by their position.
351                    collected_items.sort_unstable_by_key(|(_item, index)| *index);
352
353                    result.push(RawChunk {
354                        content: ChunkContent::Items(
355                            collected_items.into_iter().map(|(item, _index)| item).collect(),
356                        ),
357                        previous: chunk_row.previous_chunk,
358                        identifier: chunk_row.chunk,
359                        next: chunk_row.next_chunk,
360                    });
361                }
362
363                Either::Gap(gap) => {
364                    assert!(items.next().is_some(), "we just peeked the gap");
365
366                    // We shouldn't have more than one item row for this chunk.
367                    if items.next().is_some() {
368                        return Err(format!(
369                            "there shouldn't be more than one item row attached in gap chunk {}",
370                            chunk_row.chunk.index()
371                        ));
372                    }
373
374                    result.push(RawChunk {
375                        content: ChunkContent::Gap(gap.clone()),
376                        previous: chunk_row.previous_chunk,
377                        identifier: chunk_row.chunk,
378                        next: chunk_row.next_chunk,
379                    });
380                }
381            }
382        }
383
384        Ok(result)
385    }
386}
387
388impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
389    fn default() -> Self {
390        Self::new()
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use ruma::room_id;
397
398    use super::{ChunkIdentifier as CId, *};
399    use crate::linked_chunk::LinkedChunkBuilder;
400
401    #[test]
402    fn test_new_items_chunk() {
403        let room_id = room_id!("!r0:matrix.org");
404        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
405
406        relational_linked_chunk.apply_updates(
407            room_id,
408            vec![
409                // 0
410                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
411                // 1 after 0
412                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
413                // 2 before 0
414                Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
415                // 3 between 2 and 0
416                Update::NewItemsChunk {
417                    previous: Some(CId::new(2)),
418                    new: CId::new(3),
419                    next: Some(CId::new(0)),
420                },
421            ],
422        );
423
424        // Chunks are correctly linked.
425        assert_eq!(
426            relational_linked_chunk.chunks,
427            &[
428                ChunkRow {
429                    room_id: room_id.to_owned(),
430                    previous_chunk: Some(CId::new(3)),
431                    chunk: CId::new(0),
432                    next_chunk: Some(CId::new(1))
433                },
434                ChunkRow {
435                    room_id: room_id.to_owned(),
436                    previous_chunk: Some(CId::new(0)),
437                    chunk: CId::new(1),
438                    next_chunk: None
439                },
440                ChunkRow {
441                    room_id: room_id.to_owned(),
442                    previous_chunk: None,
443                    chunk: CId::new(2),
444                    next_chunk: Some(CId::new(3))
445                },
446                ChunkRow {
447                    room_id: room_id.to_owned(),
448                    previous_chunk: Some(CId::new(2)),
449                    chunk: CId::new(3),
450                    next_chunk: Some(CId::new(0))
451                },
452            ],
453        );
454        // Items have not been modified.
455        assert!(relational_linked_chunk.items.is_empty());
456    }
457
458    #[test]
459    fn test_new_gap_chunk() {
460        let room_id = room_id!("!r0:matrix.org");
461        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
462
463        relational_linked_chunk.apply_updates(
464            room_id,
465            vec![
466                // 0
467                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
468                // 1 after 0
469                Update::NewGapChunk {
470                    previous: Some(CId::new(0)),
471                    new: CId::new(1),
472                    next: None,
473                    gap: (),
474                },
475                // 2 after 1
476                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
477            ],
478        );
479
480        // Chunks are correctly linked.
481        assert_eq!(
482            relational_linked_chunk.chunks,
483            &[
484                ChunkRow {
485                    room_id: room_id.to_owned(),
486                    previous_chunk: None,
487                    chunk: CId::new(0),
488                    next_chunk: Some(CId::new(1))
489                },
490                ChunkRow {
491                    room_id: room_id.to_owned(),
492                    previous_chunk: Some(CId::new(0)),
493                    chunk: CId::new(1),
494                    next_chunk: Some(CId::new(2))
495                },
496                ChunkRow {
497                    room_id: room_id.to_owned(),
498                    previous_chunk: Some(CId::new(1)),
499                    chunk: CId::new(2),
500                    next_chunk: None
501                },
502            ],
503        );
504        // Items contains the gap.
505        assert_eq!(
506            relational_linked_chunk.items,
507            &[ItemRow {
508                room_id: room_id.to_owned(),
509                position: Position::new(CId::new(1), 0),
510                item: Either::Gap(())
511            }],
512        );
513    }
514
515    #[test]
516    fn test_remove_chunk() {
517        let room_id = room_id!("!r0:matrix.org");
518        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
519
520        relational_linked_chunk.apply_updates(
521            room_id,
522            vec![
523                // 0
524                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
525                // 1 after 0
526                Update::NewGapChunk {
527                    previous: Some(CId::new(0)),
528                    new: CId::new(1),
529                    next: None,
530                    gap: (),
531                },
532                // 2 after 1
533                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
534                // remove 1
535                Update::RemoveChunk(CId::new(1)),
536            ],
537        );
538
539        // Chunks are correctly linked.
540        assert_eq!(
541            relational_linked_chunk.chunks,
542            &[
543                ChunkRow {
544                    room_id: room_id.to_owned(),
545                    previous_chunk: None,
546                    chunk: CId::new(0),
547                    next_chunk: Some(CId::new(2))
548                },
549                ChunkRow {
550                    room_id: room_id.to_owned(),
551                    previous_chunk: Some(CId::new(0)),
552                    chunk: CId::new(2),
553                    next_chunk: None
554                },
555            ],
556        );
557        // Items no longer contains the gap.
558        assert!(relational_linked_chunk.items.is_empty());
559    }
560
561    #[test]
562    fn test_push_items() {
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                // new chunk (this is not mandatory for this test, but let's try to be realistic)
570                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
571                // new items on 0
572                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
573                // new chunk (to test new items are pushed in the correct chunk)
574                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
575                // new items on 1
576                Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
577                // new items on 0 again
578                Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
579            ],
580        );
581
582        // Chunks are correctly linked.
583        assert_eq!(
584            relational_linked_chunk.chunks,
585            &[
586                ChunkRow {
587                    room_id: room_id.to_owned(),
588                    previous_chunk: None,
589                    chunk: CId::new(0),
590                    next_chunk: Some(CId::new(1))
591                },
592                ChunkRow {
593                    room_id: room_id.to_owned(),
594                    previous_chunk: Some(CId::new(0)),
595                    chunk: CId::new(1),
596                    next_chunk: None
597                },
598            ],
599        );
600        // Items contains the pushed items.
601        assert_eq!(
602            relational_linked_chunk.items,
603            &[
604                ItemRow {
605                    room_id: room_id.to_owned(),
606                    position: Position::new(CId::new(0), 0),
607                    item: Either::Item('a')
608                },
609                ItemRow {
610                    room_id: room_id.to_owned(),
611                    position: Position::new(CId::new(0), 1),
612                    item: Either::Item('b')
613                },
614                ItemRow {
615                    room_id: room_id.to_owned(),
616                    position: Position::new(CId::new(0), 2),
617                    item: Either::Item('c')
618                },
619                ItemRow {
620                    room_id: room_id.to_owned(),
621                    position: Position::new(CId::new(1), 0),
622                    item: Either::Item('x')
623                },
624                ItemRow {
625                    room_id: room_id.to_owned(),
626                    position: Position::new(CId::new(1), 1),
627                    item: Either::Item('y')
628                },
629                ItemRow {
630                    room_id: room_id.to_owned(),
631                    position: Position::new(CId::new(1), 2),
632                    item: Either::Item('z')
633                },
634                ItemRow {
635                    room_id: room_id.to_owned(),
636                    position: Position::new(CId::new(0), 3),
637                    item: Either::Item('d')
638                },
639                ItemRow {
640                    room_id: room_id.to_owned(),
641                    position: Position::new(CId::new(0), 4),
642                    item: Either::Item('e')
643                },
644            ],
645        );
646    }
647
648    #[test]
649    fn test_remove_item() {
650        let room_id = room_id!("!r0:matrix.org");
651        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
652
653        relational_linked_chunk.apply_updates(
654            room_id,
655            vec![
656                // new chunk (this is not mandatory for this test, but let's try to be realistic)
657                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
658                // new items on 0
659                Update::PushItems {
660                    at: Position::new(CId::new(0), 0),
661                    items: vec!['a', 'b', 'c', 'd', 'e'],
662                },
663                // remove an item: 'a'
664                Update::RemoveItem { at: Position::new(CId::new(0), 0) },
665                // remove an item: 'd'
666                Update::RemoveItem { at: Position::new(CId::new(0), 2) },
667            ],
668        );
669
670        // Chunks are correctly linked.
671        assert_eq!(
672            relational_linked_chunk.chunks,
673            &[ChunkRow {
674                room_id: room_id.to_owned(),
675                previous_chunk: None,
676                chunk: CId::new(0),
677                next_chunk: None
678            }],
679        );
680        // Items contains the pushed items.
681        assert_eq!(
682            relational_linked_chunk.items,
683            &[
684                ItemRow {
685                    room_id: room_id.to_owned(),
686                    position: Position::new(CId::new(0), 0),
687                    item: Either::Item('b')
688                },
689                ItemRow {
690                    room_id: room_id.to_owned(),
691                    position: Position::new(CId::new(0), 1),
692                    item: Either::Item('c')
693                },
694                ItemRow {
695                    room_id: room_id.to_owned(),
696                    position: Position::new(CId::new(0), 2),
697                    item: Either::Item('e')
698                },
699            ],
700        );
701    }
702
703    #[test]
704    fn test_detach_last_items() {
705        let room_id = room_id!("!r0:matrix.org");
706        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
707
708        relational_linked_chunk.apply_updates(
709            room_id,
710            vec![
711                // new chunk
712                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
713                // new chunk
714                Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
715                // new items on 0
716                Update::PushItems {
717                    at: Position::new(CId::new(0), 0),
718                    items: vec!['a', 'b', 'c', 'd', 'e'],
719                },
720                // new items on 1
721                Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
722                // detach last items on 0
723                Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
724            ],
725        );
726
727        // Chunks are correctly linked.
728        assert_eq!(
729            relational_linked_chunk.chunks,
730            &[
731                ChunkRow {
732                    room_id: room_id.to_owned(),
733                    previous_chunk: None,
734                    chunk: CId::new(0),
735                    next_chunk: Some(CId::new(1))
736                },
737                ChunkRow {
738                    room_id: room_id.to_owned(),
739                    previous_chunk: Some(CId::new(0)),
740                    chunk: CId::new(1),
741                    next_chunk: None
742                },
743            ],
744        );
745        // Items contains the pushed items.
746        assert_eq!(
747            relational_linked_chunk.items,
748            &[
749                ItemRow {
750                    room_id: room_id.to_owned(),
751                    position: Position::new(CId::new(0), 0),
752                    item: Either::Item('a')
753                },
754                ItemRow {
755                    room_id: room_id.to_owned(),
756                    position: Position::new(CId::new(0), 1),
757                    item: Either::Item('b')
758                },
759                ItemRow {
760                    room_id: room_id.to_owned(),
761                    position: Position::new(CId::new(1), 0),
762                    item: Either::Item('x')
763                },
764                ItemRow {
765                    room_id: room_id.to_owned(),
766                    position: Position::new(CId::new(1), 1),
767                    item: Either::Item('y')
768                },
769                ItemRow {
770                    room_id: room_id.to_owned(),
771                    position: Position::new(CId::new(1), 2),
772                    item: Either::Item('z')
773                },
774            ],
775        );
776    }
777
778    #[test]
779    fn test_start_and_end_reattach_items() {
780        let room_id = room_id!("!r0:matrix.org");
781        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
782
783        relational_linked_chunk
784            .apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
785
786        // Nothing happened.
787        assert!(relational_linked_chunk.chunks.is_empty());
788        assert!(relational_linked_chunk.items.is_empty());
789    }
790
791    #[test]
792    fn test_clear() {
793        let r0 = room_id!("!r0:matrix.org");
794        let r1 = room_id!("!r1:matrix.org");
795        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
796
797        relational_linked_chunk.apply_updates(
798            r0,
799            vec![
800                // new chunk (this is not mandatory for this test, but let's try to be realistic)
801                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
802                // new items on 0
803                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
804            ],
805        );
806
807        relational_linked_chunk.apply_updates(
808            r1,
809            vec![
810                // new chunk (this is not mandatory for this test, but let's try to be realistic)
811                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
812                // new items on 0
813                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x'] },
814            ],
815        );
816
817        // Chunks are correctly linked.
818        assert_eq!(
819            relational_linked_chunk.chunks,
820            &[
821                ChunkRow {
822                    room_id: r0.to_owned(),
823                    previous_chunk: None,
824                    chunk: CId::new(0),
825                    next_chunk: None,
826                },
827                ChunkRow {
828                    room_id: r1.to_owned(),
829                    previous_chunk: None,
830                    chunk: CId::new(0),
831                    next_chunk: None,
832                }
833            ],
834        );
835
836        // Items contains the pushed items.
837        assert_eq!(
838            relational_linked_chunk.items,
839            &[
840                ItemRow {
841                    room_id: r0.to_owned(),
842                    position: Position::new(CId::new(0), 0),
843                    item: Either::Item('a')
844                },
845                ItemRow {
846                    room_id: r0.to_owned(),
847                    position: Position::new(CId::new(0), 1),
848                    item: Either::Item('b')
849                },
850                ItemRow {
851                    room_id: r0.to_owned(),
852                    position: Position::new(CId::new(0), 2),
853                    item: Either::Item('c')
854                },
855                ItemRow {
856                    room_id: r1.to_owned(),
857                    position: Position::new(CId::new(0), 0),
858                    item: Either::Item('x')
859                },
860            ],
861        );
862
863        // Now, time for a clean up.
864        relational_linked_chunk.apply_updates(r0, vec![Update::Clear]);
865
866        // Only items from r1 remain.
867        assert_eq!(
868            relational_linked_chunk.chunks,
869            &[ChunkRow {
870                room_id: r1.to_owned(),
871                previous_chunk: None,
872                chunk: CId::new(0),
873                next_chunk: None,
874            }],
875        );
876
877        assert_eq!(
878            relational_linked_chunk.items,
879            &[ItemRow {
880                room_id: r1.to_owned(),
881                position: Position::new(CId::new(0), 0),
882                item: Either::Item('x')
883            },],
884        );
885    }
886
887    #[test]
888    fn test_reload_empty_linked_chunk() {
889        let room_id = room_id!("!r0:matrix.org");
890
891        // When I reload the linked chunk components from an empty store,
892        let relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
893        let result = relational_linked_chunk.reload_chunks(room_id).unwrap();
894        assert!(result.is_empty());
895    }
896
897    #[test]
898    fn test_reload_linked_chunk_with_empty_items() {
899        let room_id = room_id!("!r0:matrix.org");
900
901        let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
902
903        // When I store an empty items chunks,
904        relational_linked_chunk.apply_updates(
905            room_id,
906            vec![Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }],
907        );
908
909        // It correctly gets reloaded as such.
910        let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
911        let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
912            .build()
913            .expect("building succeeds")
914            .expect("this leads to a non-empty linked chunk");
915
916        assert_items_eq!(lc, []);
917    }
918
919    #[test]
920    fn test_rebuild_linked_chunk() {
921        let room_id = room_id!("!r0:matrix.org");
922        let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
923
924        relational_linked_chunk.apply_updates(
925            room_id,
926            vec![
927                // new chunk
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!['a', 'b', 'c'] },
931                // a gap chunk
932                Update::NewGapChunk {
933                    previous: Some(CId::new(0)),
934                    new: CId::new(1),
935                    next: None,
936                    gap: 'g',
937                },
938                // another items chunk
939                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
940                // new items on 0
941                Update::PushItems { at: Position::new(CId::new(2), 0), items: vec!['d', 'e', 'f'] },
942            ],
943        );
944
945        let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
946        let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
947            .build()
948            .expect("building succeeds")
949            .expect("this leads to a non-empty linked chunk");
950
951        // The linked chunk is correctly reloaded.
952        assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
953    }
954
955    #[test]
956    fn test_replace_item() {
957        let room_id = room_id!("!r0:matrix.org");
958        let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
959
960        relational_linked_chunk.apply_updates(
961            room_id,
962            vec![
963                // new chunk (this is not mandatory for this test, but let's try to be realistic)
964                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
965                // new items on 0
966                Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
967                // update item at (0; 1).
968                Update::ReplaceItem { at: Position::new(CId::new(0), 1), item: 'B' },
969            ],
970        );
971
972        // Chunks are correctly linked.
973        assert_eq!(
974            relational_linked_chunk.chunks,
975            &[ChunkRow {
976                room_id: room_id.to_owned(),
977                previous_chunk: None,
978                chunk: CId::new(0),
979                next_chunk: None,
980            },],
981        );
982
983        // Items contains the pushed *and* replaced items.
984        assert_eq!(
985            relational_linked_chunk.items,
986            &[
987                ItemRow {
988                    room_id: room_id.to_owned(),
989                    position: Position::new(CId::new(0), 0),
990                    item: Either::Item('a')
991                },
992                ItemRow {
993                    room_id: room_id.to_owned(),
994                    position: Position::new(CId::new(0), 1),
995                    item: Either::Item('B')
996                },
997                ItemRow {
998                    room_id: room_id.to_owned(),
999                    position: Position::new(CId::new(0), 2),
1000                    item: Either::Item('c')
1001                },
1002            ],
1003        );
1004    }
1005}