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}