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