Skip to main content

matrix_sdk_common/linked_chunk/
relational.rs

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