Skip to main content

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}