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