yo/db.rs
1//! The database, the one call that opens it, and the handle everything else
2//! reaches it through.
3
4use core::cell::RefCell;
5use std::rc::Rc;
6
7use yo_common::{Code, Error, Result};
8use yo_index::RawMap;
9use yo_shape::{Desc, Tag};
10
11use crate::counter::Counter;
12use crate::keys::Keys;
13use crate::keyspace::Strings;
14use crate::map::Map;
15use crate::sets::{Set, Sets};
16use crate::store::Decode;
17
18/// The path that means "no file at all", which is a real path and not a flag
19/// (`07` section 7).
20pub const MEMORY: &str = ":memory:";
21
22/// Open a database.
23///
24/// That is the whole setup. There is no database to build before a connection,
25/// no configuration to pass, no engine to choose, and no pool. The engine is
26/// inferred from the path, and [`MEMORY`] is a path.
27///
28/// # Errors
29///
30/// [`Code::Unsupported`] for a path on disk, until the file format lands in
31/// M5. Everything else about the API is the same either way, which is the
32/// point of putting the front door in before the file.
33pub fn open(path: &str) -> Result<Db> {
34 if path != MEMORY {
35 return Err(Error::fmt(
36 Code::Unsupported,
37 format_args!(
38 "this build holds a database in memory only, so the path has to be \"{MEMORY}\", not \"{path}\". A file backed database arrives with the .yo format in M5"
39 ),
40 ));
41 }
42 Ok(Db {
43 db: Handle {
44 inner: Rc::new(RefCell::new(Inner {
45 collections: Vec::new(),
46 strings: yo_kv::Keyspace::new(),
47 deadlines: false,
48 })),
49 },
50 })
51}
52
53/// An open database.
54///
55/// Cheap to clone, and every clone is the same database. A handle taken out of
56/// it stays valid for as long as any clone lives.
57///
58/// This build runs in inline mode (`15` section 7): the calling thread is the
59/// shard, which is what makes a point read a function call rather than a
60/// message. That is also why a handle does not cross threads yet. The owned
61/// and served modes put the same API on top of `yo-shard`'s runtime, and they
62/// arrive with it.
63#[derive(Clone)]
64pub struct Db {
65 db: Handle,
66}
67
68impl core::fmt::Debug for Db {
69 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70 let mut d = f.debug_struct("Db");
71 match self.collections() {
72 Ok(names) => d.field("collections", &names).finish(),
73 Err(_) => d.finish_non_exhaustive(),
74 }
75 }
76}
77
78/// The database itself, which one thread owns and reaches through [`Handle`].
79pub(crate) struct Inner {
80 pub(crate) collections: Vec<Collection>,
81 pub(crate) strings: yo_kv::Keyspace,
82 /// Whether any key has ever been given a deadline, which is exactly when
83 /// the clock's answer can be observed. See `keyspace`'s module docs.
84 pub(crate) deadlines: bool,
85}
86
87pub(crate) struct Collection {
88 pub(crate) name: String,
89 pub(crate) desc: Desc,
90 pub(crate) data: RawMap,
91}
92
93/// A shared, cheap pointer to one database.
94///
95/// Every handle the user holds is one of these plus whatever names the thing
96/// it points at, so a `Map` is two words and an index and a `Counter` is two
97/// words and a key.
98#[derive(Clone)]
99pub(crate) struct Handle {
100 inner: Rc<RefCell<Inner>>,
101}
102
103impl Handle {
104 /// Run something against the database, with the clock brought up to date
105 /// first if any deadline exists to compare against.
106 pub(crate) fn run<R>(&self, f: impl FnOnce(&mut Inner) -> Result<R>) -> Result<R> {
107 let mut inner = self.inner.try_borrow_mut().map_err(|_| reentrant())?;
108 if inner.deadlines {
109 inner.strings.clock_mut().refresh();
110 }
111 f(&mut inner)
112 }
113
114 /// The same, for something that is about to create a deadline.
115 pub(crate) fn deadlines<R>(&self, f: impl FnOnce(&mut Inner) -> Result<R>) -> Result<R> {
116 let mut inner = self.inner.try_borrow_mut().map_err(|_| reentrant())?;
117 inner.deadlines = true;
118 inner.strings.clock_mut().refresh();
119 f(&mut inner)
120 }
121
122 /// A shared look at the database, which is what a read of a typed
123 /// collection needs and nothing more.
124 pub(crate) fn read<R>(&self, f: impl FnOnce(&Inner) -> Result<R>) -> Result<R> {
125 let inner = self.inner.try_borrow().map_err(|_| reentrant())?;
126 f(&inner)
127 }
128
129 /// A write that no deadline can be observed through, which is every write
130 /// to a typed collection so far. The clock is left where it is.
131 pub(crate) fn write<R>(&self, f: impl FnOnce(&mut Inner) -> Result<R>) -> Result<R> {
132 let mut inner = self.inner.try_borrow_mut().map_err(|_| reentrant())?;
133 f(&mut inner)
134 }
135
136 /// Whether two handles point at the same database.
137 pub(crate) fn is(&self, other: &Handle) -> bool {
138 Rc::ptr_eq(&self.inner, &other.inner)
139 }
140}
141
142/// The error a call made from inside another call's callback gets.
143///
144/// A database that panics because of how the caller nested two of its own
145/// methods is a database people stop trusting, so re-entrancy is an error
146/// value with a sentence attached rather than a `RefCell` panic.
147pub(crate) fn reentrant() -> Error {
148 Error::new(
149 Code::Invalid,
150 "this database is already in use by the call above this one. A closure passed to with() or update() cannot call back into the same database, so read what you need first and write after the closure returns",
151 )
152}
153
154impl Db {
155 /// Open a map, creating it if this is the first time.
156 ///
157 /// The type is the collection's shape (`15` section 3), so opening the
158 /// same name a second time with a different type is an error and not a
159 /// surprise later: the shapes are compared, and a mismatch says which
160 /// field moved and whether the change is additive or breaking.
161 ///
162 /// # Errors
163 ///
164 /// [`Code::ShapeMismatch`] when the name is already a collection of
165 /// another shape.
166 pub fn map<K: Decode, V: Decode>(&self, name: &str) -> Result<Map<K, V>> {
167 let mut desc = Desc::new();
168 desc.map(K::describe, V::describe);
169 let tag = desc.tag();
170
171 let at =
172 self.db.write(
173 |inner| match inner.collections.iter().position(|c| c.name == name) {
174 Some(at) => {
175 yo_shape::check(name, &inner.collections[at].desc, &desc, None)?;
176 Ok(at)
177 }
178 None => {
179 inner.collections.push(Collection {
180 name: name.to_owned(),
181 desc,
182 data: RawMap::new(),
183 });
184 Ok(inner.collections.len() - 1)
185 }
186 },
187 )?;
188 Ok(Map::new(self.db.clone(), at, tag))
189 }
190
191 /// The Redis string keyspace.
192 ///
193 /// The same store a client reaches over RESP, reached without the socket,
194 /// the parser or the reply (Y23). Not a named collection, because in Redis
195 /// a string is not one: it is the keyspace itself.
196 ///
197 /// ```
198 /// let db = yo::open(yo::MEMORY)?;
199 /// assert_eq!(db.strings().incr("hits")?, 1);
200 /// # Ok::<(), yo::Error>(())
201 /// ```
202 #[must_use]
203 pub fn strings(&self) -> Strings {
204 Strings {
205 db: self.db.clone(),
206 }
207 }
208
209 /// A counter at one key, which is `15` section 2's `db.counter("hits")`.
210 ///
211 /// Sugar over [`Db::strings`] and worth having: a counter is the commonest
212 /// thing a string key is, and a handle that holds the key means the key is
213 /// spelled once rather than at every call site.
214 ///
215 /// ```
216 /// let db = yo::open(yo::MEMORY)?;
217 /// let hits = db.counter("hits");
218 ///
219 /// hits.incr()?;
220 /// hits.add(9)?;
221 /// assert_eq!(hits.get()?, 10);
222 /// # Ok::<(), yo::Error>(())
223 /// ```
224 #[must_use]
225 pub fn counter(&self, key: impl Into<Vec<u8>>) -> Counter {
226 Counter {
227 db: self.db.clone(),
228 key: key.into(),
229 }
230 }
231
232 /// Every Redis set command, with the key as the first argument.
233 ///
234 /// The same store `SADD` off a socket reaches. Like [`Db::strings`] this is
235 /// not a named collection, because in Redis a set is not one: it is a key in
236 /// the keyspace that happens to hold a set.
237 ///
238 /// ```
239 /// let db = yo::open(yo::MEMORY)?;
240 /// db.sets().add_many("online", &["alice", "bob"])?;
241 /// assert_eq!(db.sets().len_of("online")?, 2);
242 /// # Ok::<(), yo::Error>(())
243 /// ```
244 #[must_use]
245 pub fn sets(&self) -> Sets {
246 Sets {
247 db: self.db.clone(),
248 }
249 }
250
251 /// A set at one key, which is the same sugar [`Db::counter`] is.
252 ///
253 /// ```
254 /// let db = yo::open(yo::MEMORY)?;
255 /// let online = db.set("online");
256 ///
257 /// online.add("alice")?;
258 /// assert!(online.contains("alice")?);
259 /// # Ok::<(), yo::Error>(())
260 /// ```
261 #[must_use]
262 pub fn set(&self, key: impl Into<Vec<u8>>) -> Set {
263 Set {
264 sets: self.sets(),
265 key: key.into(),
266 }
267 }
268
269 /// Every command that works on a key whatever the key holds.
270 ///
271 /// `DEL`, `EXISTS` and `TYPE`, and the whole expiry family. These are the
272 /// ones that belong to the keyspace rather than to a type, which is why
273 /// they are not on [`Db::strings`] or [`Db::sets`]: a deadline sits in the
274 /// key's record and does not care what the record points at.
275 ///
276 /// ```
277 /// use std::time::Duration;
278 ///
279 /// let db = yo::open(yo::MEMORY)?;
280 /// db.set("online").add("alice")?;
281 /// db.keys().expire_in("online", Duration::from_secs(60))?;
282 /// # Ok::<(), yo::Error>(())
283 /// ```
284 #[must_use]
285 pub fn keys(&self) -> Keys {
286 Keys {
287 db: self.db.clone(),
288 }
289 }
290
291 /// The names of the typed collections in this database, in the order they
292 /// were first opened.
293 ///
294 /// # Errors
295 ///
296 /// [`Code::Invalid`] if called from inside a callback that is already
297 /// holding this database.
298 pub fn collections(&self) -> Result<Vec<String>> {
299 self.db
300 .read(|inner| Ok(inner.collections.iter().map(|c| c.name.clone()).collect()))
301 }
302
303 /// The shape of a collection, if it exists.
304 ///
305 /// # Errors
306 ///
307 /// [`Code::Invalid`] if called from inside a callback that is already
308 /// holding this database.
309 pub fn shape(&self, name: &str) -> Result<Option<Tag>> {
310 self.db.read(|inner| {
311 Ok(inner
312 .collections
313 .iter()
314 .find(|c| c.name == name)
315 .map(|c| c.desc.tag()))
316 })
317 }
318
319 /// What this database is holding, index and arena together, across the
320 /// keyspace and every typed collection.
321 ///
322 /// # Errors
323 ///
324 /// [`Code::Invalid`] if called from inside a callback that is already
325 /// holding this database.
326 pub fn memory_bytes(&self) -> Result<usize> {
327 self.db.read(|inner| {
328 Ok(inner.strings.memory_bytes()
329 + inner
330 .collections
331 .iter()
332 .map(|c| c.data.memory_bytes())
333 .sum::<usize>())
334 })
335 }
336
337 /// Whether this database reads the clock on the data path.
338 ///
339 /// False until something is given a deadline, because until then the
340 /// clock's answer cannot change any reply. `04` section 5 is the reason
341 /// this is worth a method: a clock read is tens of nanoseconds against a
342 /// budget of a hundred and fifty.
343 #[must_use]
344 pub fn reads_the_clock(&self) -> bool {
345 self.db.read(|inner| Ok(inner.deadlines)).unwrap_or(false)
346 }
347
348 /// Whether two databases are the same one.
349 #[must_use]
350 pub fn is(&self, other: &Db) -> bool {
351 self.db.is(&other.db)
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn a_path_on_disk_says_which_build_would_take_it() {
361 let e = open("app.yo").expect_err("no file format yet");
362 assert_eq!(e.code(), Code::Unsupported);
363 assert!(e.message().contains("M5"), "{e}");
364 }
365
366 #[test]
367 fn opening_the_same_name_twice_gives_the_same_collection() {
368 let db = open(MEMORY).unwrap();
369 let a = db.map::<String, u64>("hits").unwrap();
370 let b = db.map::<String, u64>("hits").unwrap();
371 a.set("home", &1).unwrap();
372 assert_eq!(b.get("home").unwrap(), Some(1));
373 assert_eq!(db.collections().unwrap(), vec!["hits".to_owned()]);
374 }
375
376 /// The whole reason the tag exists, from the caller's side: the second
377 /// open does not quietly hand back a map that reads other people's bytes
378 /// as its own type.
379 #[test]
380 fn opening_the_same_name_with_another_type_is_a_shape_mismatch() {
381 let db = open(MEMORY).unwrap();
382 let _first = db.map::<String, u64>("hits").unwrap();
383 let e = db
384 .map::<String, String>("hits")
385 .expect_err("that is a different shape");
386 assert_eq!(e.code(), Code::ShapeMismatch);
387 assert!(
388 e.message().contains("the type changed from u64 to str"),
389 "{e}"
390 );
391 assert_eq!(e.detail(), Some("change=breaking"));
392 }
393
394 #[test]
395 fn two_collections_are_two_keyspaces() {
396 let db = open(MEMORY).unwrap();
397 let a = db.map::<String, u64>("a").unwrap();
398 let b = db.map::<String, u64>("b").unwrap();
399 a.set("k", &1).unwrap();
400 b.set("k", &2).unwrap();
401 assert_eq!(a.get("k").unwrap(), Some(1));
402 assert_eq!(b.get("k").unwrap(), Some(2));
403 assert_eq!(db.collections().unwrap().len(), 2);
404 }
405
406 /// A typed collection and the Redis keyspace do not see each other, which
407 /// is what the catalogue in `07` section 5 says: a collection is a name,
408 /// and the string type is the keyspace.
409 #[test]
410 fn a_typed_collection_and_the_keyspace_are_not_the_same_store() {
411 let db = open(MEMORY).unwrap();
412 let map = db.map::<String, u64>("hits").unwrap();
413 map.set("home", &1).unwrap();
414 db.strings().set("home", "elsewhere").unwrap();
415
416 assert_eq!(map.get("home").unwrap(), Some(1));
417 assert_eq!(
418 db.strings().get("home").unwrap().as_deref(),
419 Some(&b"elsewhere"[..])
420 );
421 }
422
423 #[test]
424 fn a_shape_can_be_read_back_and_an_unopened_name_has_none() {
425 let db = open(MEMORY).unwrap();
426 let map = db.map::<String, u64>("hits").unwrap();
427 assert_eq!(db.shape("hits").unwrap(), Some(map.tag()));
428 assert_eq!(db.shape("misses").unwrap(), None);
429 }
430
431 #[test]
432 fn a_clone_is_the_same_database() {
433 let db = open(MEMORY).unwrap();
434 let map = db.map::<String, u64>("hits").unwrap();
435 map.set("home", &3).unwrap();
436 let same = db.clone();
437 assert_eq!(
438 same.map::<String, u64>("hits")
439 .unwrap()
440 .get("home")
441 .unwrap(),
442 Some(3)
443 );
444 assert!(db.is(&same));
445 assert!(!db.is(&open(MEMORY).unwrap()));
446 assert!(db.memory_bytes().unwrap() > 0);
447 assert!(format!("{db:?}").contains("hits"));
448 }
449}