Skip to main content

matrix_sdk_common/linked_chunk/
relational.rs

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