cnidarium/store/
multistore.rs

1use std::{fmt::Display, sync::Arc};
2
3use super::substore::SubstoreConfig;
4
5/// A collection of substore, each with a unique prefix.
6#[derive(Debug, Clone)]
7pub struct MultistoreConfig {
8    pub main_store: Arc<SubstoreConfig>,
9    pub substores: Vec<Arc<SubstoreConfig>>,
10}
11
12impl MultistoreConfig {
13    pub fn iter(&self) -> impl Iterator<Item = &Arc<SubstoreConfig>> {
14        self.substores.iter()
15    }
16
17    /// Returns the substore matching the key's prefix, return `None` otherwise.
18    pub fn find_substore(&self, key: &[u8]) -> Option<Arc<SubstoreConfig>> {
19        if key.is_empty() {
20            return Some(self.main_store.clone());
21        }
22
23        // Note: This is a linear search, but the number of substores is small.
24        self.substores
25            .iter()
26            .find(|s| key.starts_with(s.prefix.as_bytes()))
27            .cloned()
28    }
29
30    /// Route a key to a substore, and return the truncated key and the corresponding `SubstoreConfig`.
31    ///
32    /// This method is used for ordinary key-value operations.
33    ///
34    /// Note: since this method implements the routing logic for the multistore,
35    /// callers might prefer [`MultistoreConfig::match_prefix_str`] if they don't
36    /// need to route the key.
37    ///
38    /// # Routing
39    /// + If the key is a total match for the prefix, the **main store** is returned.
40    /// + If the key is not a total match for the prefix, the prefix is removed from  
41    ///   the key and the key is routed to the substore matching the prefix.
42    /// + If the key does not match any prefix, the key is routed to the **main store**.
43    /// + If a delimiter is prefixing the key, it is removed.
44    ///
45    /// # Examples
46    /// `prefix_a/key` -> `key` in `substore_a`
47    /// `prefix_akey` -> `prefix_akey` in `main_store
48    /// `prefix_a` -> `prefix_a` in `main_store`
49    /// `prefix_a/` -> `prefix_a/` in `main_store
50    /// `nonexistent_prefix` -> `nonexistent_prefix` in `main_store`
51    pub fn route_key_str<'a>(&self, key: &'a str) -> (&'a str, Arc<SubstoreConfig>) {
52        let config = self
53            .find_substore(key.as_bytes())
54            .unwrap_or_else(|| self.main_store.clone());
55
56        // If the key is a total match, we want to return the key bound to the
57        // main store. This is where the root hash of the prefix tree is located.
58        if key == config.prefix {
59            return (key, self.main_store.clone());
60        }
61
62        let truncated_key = key
63            .strip_prefix(&config.prefix)
64            .expect("key has the prefix of the matched substore");
65
66        // If the key does not contain a delimiter, we return the original key
67        // routed to the main store. This is because we do not want to allow
68        // collisions e.g. `prefix_a/key` and `prefix_akey`.
69        let Some(matching_key) = truncated_key.strip_prefix('/') else {
70            return (key, self.main_store.clone());
71        };
72
73        // If the matching key is empty, we return the original key routed to
74        // the main store. This is because we do not want to allow empty keys
75        // in the substore.
76        if matching_key.is_empty() {
77            (key, self.main_store.clone())
78        } else {
79            (matching_key, config)
80        }
81    }
82
83    /// Route a key to a substore, and return the truncated key and the corresponding `SubstoreConfig`.
84    ///
85    /// This method is used for ordinary key-value operations.
86    ///
87    /// Note: since this method implements the routing logic for the multistore,
88    /// callers might prefer [`MultistoreConfig::match_prefix_bytes`] if they don't
89    /// need to route the key.
90    ///
91    /// # Routing
92    /// + If the key is a total match for the prefix, the **main store** is returned.
93    /// + If the key is not a total match for the prefix, the prefix is removed from  
94    ///   the key and the key is routed to the substore matching the prefix.
95    /// + If the key does not match any prefix, the key is routed to the **main store**.
96    /// + If a delimiter is prefixing the key, it is removed.
97    ///
98    /// # Examples
99    /// `prefix_a/key` -> `key` in `substore_a`
100    /// `prefix_a` -> `prefix_a` in `main_store`
101    /// `prefix_a/` -> `prefix_a/` in `main_store`
102    /// `nonexistent_prefix` -> `nonexistent_prefix` in `main_store`
103    pub fn route_key_bytes<'a>(&self, key: &'a [u8]) -> (&'a [u8], Arc<SubstoreConfig>) {
104        let config = self
105            .find_substore(key)
106            .unwrap_or_else(|| self.main_store.clone());
107
108        // If the key is a total match for the prefix, we return the original key
109        // routed to the main store. This is where subtree root hashes are stored.
110        if key == config.prefix.as_bytes() {
111            return (key, self.main_store.clone());
112        }
113
114        let truncated_key = key
115            .strip_prefix(config.prefix.as_bytes())
116            .expect("key has the prefix of the matched substore");
117
118        // If the key does not contain a delimiter, we return the original key
119        // routed to the main store. This is because we do not want to allow
120        // collisions e.g. `prefix_a/key` and `prefix_akey`.
121        let Some(matching_key) = truncated_key.strip_prefix(b"/") else {
122            return (key, self.main_store.clone());
123        };
124
125        // If the matching key is empty, we return the original key routed to
126        // the main store. This is because we do not want to allow empty keys
127        // in the substore.
128        if matching_key.is_empty() {
129            (key, self.main_store.clone())
130        } else {
131            (matching_key, config)
132        }
133    }
134
135    /// Returns the truncated prefix and the corresponding `SubstoreConfig`.
136    ///
137    /// This method is used to implement prefix iteration.
138    ///
139    /// Unlike [`MultistoreConfig::route_key_str`], this method does not do any routing.
140    /// It simply finds the substore matching the prefix, strip the prefix and delimiter,
141    /// and returns the truncated prefix and the corresponding `SubstoreConfig`.
142    ///
143    /// # Examples
144    /// `prefix_a/key` -> `key` in `substore_a`
145    /// `prefix_a` -> "" in `substore_a`
146    /// `prefix_a/` -> "" in `substore_a`
147    /// `nonexistent_prefix` -> "" in `main_store`
148    pub fn match_prefix_str<'a>(&self, prefix: &'a str) -> (&'a str, Arc<SubstoreConfig>) {
149        let config = self
150            .find_substore(prefix.as_bytes())
151            .unwrap_or_else(|| self.main_store.clone());
152
153        let truncated_prefix = prefix
154            .strip_prefix(&config.prefix)
155            .expect("key has the prefix of the matched substore");
156
157        let truncated_prefix = truncated_prefix
158            .strip_prefix('/')
159            .unwrap_or(truncated_prefix);
160        (truncated_prefix, config)
161    }
162
163    /// Returns the truncated prefix and the corresponding `SubstoreConfig`.
164    ///
165    /// Unlike [`MultistoreConfig::route_key_str`], this method does not do any routing.
166    /// It simply finds the substore matching the prefix, strip the prefix and delimiter,
167    /// and returns the truncated prefix and the corresponding `SubstoreConfig`.
168    ///
169    /// This method is used to implement prefix iteration.
170    ///
171    /// # Examples
172    /// `prefix_a/key` -> `key` in `substore_a`
173    /// `prefix_a` -> "" in `substore_a`
174    /// `prefix_a/` -> "" in `substore_a`
175    /// `nonexistent_prefix` -> "" in `main_store`
176    pub fn match_prefix_bytes<'a>(&self, prefix: &'a [u8]) -> (&'a [u8], Arc<SubstoreConfig>) {
177        let config = self
178            .find_substore(prefix)
179            .unwrap_or_else(|| self.main_store.clone());
180
181        let truncated_prefix = prefix
182            .strip_prefix(config.prefix.as_bytes())
183            .expect("key has the prefix of the matched substore");
184
185        let truncated_prefix = truncated_prefix
186            .strip_prefix(b"/")
187            .unwrap_or(truncated_prefix);
188        (truncated_prefix, config)
189    }
190}
191
192impl Default for MultistoreConfig {
193    fn default() -> Self {
194        Self {
195            main_store: Arc::new(SubstoreConfig::new("")),
196            substores: vec![],
197        }
198    }
199}
200
201/// Tracks the latest version of each substore, and wraps a `MultistoreConfig`.
202#[derive(Default, Debug)]
203pub struct MultistoreCache {
204    pub config: MultistoreConfig,
205    pub substores: std::collections::BTreeMap<Arc<SubstoreConfig>, jmt::Version>,
206}
207
208impl Display for MultistoreCache {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        let mut s = String::new();
211        for (substore, version) in &self.substores {
212            s.push_str(&format!("{}: {}\n", substore.prefix, version));
213        }
214        write!(f, "{}", s)
215    }
216}
217
218impl MultistoreCache {
219    pub fn from_config(config: MultistoreConfig) -> Self {
220        Self {
221            config,
222            substores: std::collections::BTreeMap::new(),
223        }
224    }
225
226    pub fn set_version(&mut self, substore: Arc<SubstoreConfig>, version: jmt::Version) {
227        self.substores.insert(substore, version);
228    }
229
230    pub fn get_version(&self, substore: &Arc<SubstoreConfig>) -> Option<jmt::Version> {
231        self.substores.get(substore).cloned()
232    }
233}