Skip to main content

matrix_sdk_common/linked_chunk/
relational.rs

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