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