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, Shape, Tag};
10
11use crate::counter::Counter;
12use crate::doc::{Docs, Document, Documents};
13use crate::graph::Graph;
14use crate::keys::Keys;
15use crate::keyspace::Strings;
16use crate::map::Map;
17use crate::sets::{Set, Sets};
18use crate::store::Decode;
19use crate::vector::Vectors;
20
21/// The path that means "no file at all", which is a real path and not a flag
22/// (`07` section 7).
23pub const MEMORY: &str = ":memory:";
24
25/// Open a database.
26///
27/// That is the whole setup. There is no database to build before a connection,
28/// no configuration to pass, no engine to choose, and no pool. The engine is
29/// inferred from the path, and [`MEMORY`] is a path.
30///
31/// # Errors
32///
33/// [`Code::Unsupported`] for a path on disk, until the file format lands in
34/// M5. Everything else about the API is the same either way, which is the
35/// point of putting the front door in before the file.
36pub fn open(path: &str) -> Result<Db> {
37    if path != MEMORY {
38        return Err(Error::fmt(
39            Code::Unsupported,
40            format_args!(
41                "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"
42            ),
43        ));
44    }
45    Ok(Db {
46        db: Handle {
47            inner: Rc::new(RefCell::new(Inner {
48                collections: Vec::new(),
49                strings: yo_kv::Keyspace::new(),
50                deadlines: false,
51            })),
52        },
53    })
54}
55
56/// An open database.
57///
58/// Cheap to clone, and every clone is the same database. A handle taken out of
59/// it stays valid for as long as any clone lives.
60///
61/// This build runs in inline mode (`15` section 7): the calling thread is the
62/// shard, which is what makes a point read a function call rather than a
63/// message. That is also why a handle does not cross threads yet. The owned
64/// and served modes put the same API on top of `yo-shard`'s runtime, and they
65/// arrive with it.
66#[derive(Clone)]
67pub struct Db {
68    db: Handle,
69}
70
71impl core::fmt::Debug for Db {
72    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
73        let mut d = f.debug_struct("Db");
74        match self.collections() {
75            Ok(names) => d.field("collections", &names).finish(),
76            Err(_) => d.finish_non_exhaustive(),
77        }
78    }
79}
80
81/// The database itself, which one thread owns and reaches through [`Handle`].
82pub(crate) struct Inner {
83    pub(crate) collections: Vec<Collection>,
84    pub(crate) strings: yo_kv::Keyspace,
85    /// Whether any key has ever been given a deadline, which is exactly when
86    /// the clock's answer can be observed. See `keyspace`'s module docs.
87    pub(crate) deadlines: bool,
88}
89
90pub(crate) struct Collection {
91    pub(crate) name: String,
92    pub(crate) desc: Desc,
93    pub(crate) data: Data,
94}
95
96/// What a collection holds, which is decided by the handle it was opened
97/// through and never changes afterwards.
98///
99/// One catalogue covers both kinds rather than two, because a name is a name:
100/// opening `orders` as a map and then as a document collection has to be the
101/// same refusal as opening it as a map of the wrong type, and it is, since the
102/// shapes differ and [`yo_shape::check`] compares them before this is reached.
103// The two variants are different sizes and that is the point. A map is the hot
104// path and it stays where it is, so the one that would have made this enum wide
105// is the one behind a pointer.
106#[allow(clippy::large_enum_variant)]
107pub(crate) enum Data {
108    Map(RawMap),
109    /// Boxed because a document collection carries its indexes and its build
110    /// buffer, and a map should not pay for the size of one.
111    Docs(Box<Documents>),
112    /// Boxed for the same reason, harder: a graph carries a plane, two document
113    /// stores and the table that turns an id into a dense one.
114    Graph(Box<crate::graph::Store>),
115    /// Boxed for the same reason again: a vector collection carries the
116    /// partitions, the codes under them and every full precision vector.
117    Vectors(Box<yo_vector::Collection>),
118}
119
120impl Data {
121    pub(crate) fn memory_bytes(&self) -> usize {
122        match self {
123            Data::Map(m) => m.memory_bytes(),
124            Data::Docs(d) => d.docs.memory_bytes(),
125            Data::Graph(g) => g.memory_bytes(),
126            Data::Vectors(v) => v.memory_bytes(),
127        }
128    }
129
130    /// The map inside, for a handle that was handed out against one.
131    ///
132    /// # Panics
133    ///
134    /// Never, from outside: a `Map<K, V>` handle only exists for a collection
135    /// whose shape is a map, and a shape cannot change under a name.
136    #[track_caller]
137    pub(crate) fn map(&self) -> &RawMap {
138        match self {
139            Data::Map(m) => m,
140            _ => wrong_kind(),
141        }
142    }
143
144    /// The same, for a write.
145    ///
146    /// # Panics
147    ///
148    /// The same as [`Data::map`].
149    #[track_caller]
150    pub(crate) fn map_mut(&mut self) -> &mut RawMap {
151        match self {
152            Data::Map(m) => m,
153            _ => wrong_kind(),
154        }
155    }
156
157    /// The documents inside, for a handle that was handed out against them.
158    ///
159    /// # Panics
160    ///
161    /// The same as [`Data::map`].
162    #[track_caller]
163    pub(crate) fn docs(&self) -> &yo_doc::Docs {
164        match self {
165            Data::Docs(d) => &d.docs,
166            _ => wrong_kind(),
167        }
168    }
169
170    /// The same, for a write.
171    ///
172    /// # Panics
173    ///
174    /// The same as [`Data::map`].
175    #[track_caller]
176    pub(crate) fn docs_mut(&mut self) -> &mut Documents {
177        match self {
178            Data::Docs(d) => d,
179            _ => wrong_kind(),
180        }
181    }
182
183    /// The graph inside, for a handle that was handed out against one.
184    ///
185    /// # Panics
186    ///
187    /// The same as [`Data::map`].
188    #[track_caller]
189    pub(crate) fn graph(&self) -> &crate::graph::Store {
190        match self {
191            Data::Graph(g) => g,
192            _ => wrong_kind(),
193        }
194    }
195
196    /// The same, for a write.
197    ///
198    /// # Panics
199    ///
200    /// The same as [`Data::map`].
201    #[track_caller]
202    pub(crate) fn graph_mut(&mut self) -> &mut crate::graph::Store {
203        match self {
204            Data::Graph(g) => g,
205            _ => wrong_kind(),
206        }
207    }
208
209    /// The vectors inside, for a handle that was handed out against them.
210    ///
211    /// # Panics
212    ///
213    /// The same as [`Data::map`].
214    #[track_caller]
215    pub(crate) fn vectors(&self) -> &yo_vector::Collection {
216        match self {
217            Data::Vectors(v) => v,
218            _ => wrong_kind(),
219        }
220    }
221
222    /// The same, for a write.
223    ///
224    /// # Panics
225    ///
226    /// The same as [`Data::map`].
227    #[track_caller]
228    pub(crate) fn vectors_mut(&mut self) -> &mut yo_vector::Collection {
229        match self {
230            Data::Vectors(v) => v,
231            _ => wrong_kind(),
232        }
233    }
234}
235
236#[track_caller]
237fn wrong_kind() -> ! {
238    panic!(
239        "this handle and the collection it names hold different things, which the shape check is there to make impossible. Please report this as a bug"
240    )
241}
242
243/// A shared, cheap pointer to one database.
244///
245/// Every handle the user holds is one of these plus whatever names the thing
246/// it points at, so a `Map` is two words and an index and a `Counter` is two
247/// words and a key.
248#[derive(Clone)]
249pub(crate) struct Handle {
250    inner: Rc<RefCell<Inner>>,
251}
252
253impl Handle {
254    /// Run something against the database, with the clock brought up to date
255    /// first if any deadline exists to compare against.
256    pub(crate) fn run<R>(&self, f: impl FnOnce(&mut Inner) -> Result<R>) -> Result<R> {
257        let mut inner = self.inner.try_borrow_mut().map_err(|_| reentrant())?;
258        if inner.deadlines {
259            inner.strings.clock().refresh();
260        }
261        f(&mut inner)
262    }
263
264    /// The same, for something that is about to create a deadline.
265    pub(crate) fn deadlines<R>(&self, f: impl FnOnce(&mut Inner) -> Result<R>) -> Result<R> {
266        let mut inner = self.inner.try_borrow_mut().map_err(|_| reentrant())?;
267        inner.deadlines = true;
268        inner.strings.clock().refresh();
269        f(&mut inner)
270    }
271
272    /// A shared look at the database, which is what a read of a typed
273    /// collection needs and nothing more.
274    pub(crate) fn read<R>(&self, f: impl FnOnce(&Inner) -> Result<R>) -> Result<R> {
275        let inner = self.inner.try_borrow().map_err(|_| reentrant())?;
276        f(&inner)
277    }
278
279    /// A write that no deadline can be observed through, which is every write
280    /// to a typed collection so far. The clock is left where it is.
281    pub(crate) fn write<R>(&self, f: impl FnOnce(&mut Inner) -> Result<R>) -> Result<R> {
282        let mut inner = self.inner.try_borrow_mut().map_err(|_| reentrant())?;
283        f(&mut inner)
284    }
285
286    /// Whether two handles point at the same database.
287    pub(crate) fn is(&self, other: &Handle) -> bool {
288        Rc::ptr_eq(&self.inner, &other.inner)
289    }
290}
291
292/// Put the indexes `T` declares on a collection.
293fn declare<T: Document>(data: &mut Documents) -> Result<()> {
294    for (path, kind) in T::INDEXES {
295        data.docs.create_index_bytes(path.as_bytes(), *kind)?;
296    }
297    for (path, dim) in T::VECTORS {
298        data.docs.create_vector_index(path, *dim)?;
299    }
300    Ok(())
301}
302
303/// The shape every graph collection has.
304///
305/// A graph's node and edge types register themselves under their labels as they
306/// are used, so what the catalogue holds is only that this name is a graph and
307/// not a map or a document collection. The per label shape check is in
308/// `graph::Store`, and it is stricter than one tuple named at open time because
309/// it catches a type that changed under a label it already used.
310struct AGraph;
311
312impl Shape for AGraph {
313    fn describe(d: &mut Desc) {
314        d.strukt("graph", &[]);
315    }
316}
317
318/// The error a call made from inside another call's callback gets.
319///
320/// A database that panics because of how the caller nested two of its own
321/// methods is a database people stop trusting, so re-entrancy is an error
322/// value with a sentence attached rather than a `RefCell` panic.
323pub(crate) fn reentrant() -> Error {
324    Error::new(
325        Code::Invalid,
326        "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",
327    )
328}
329
330impl Db {
331    /// Open a map, creating it if this is the first time.
332    ///
333    /// The type is the collection's shape (`15` section 3), so opening the
334    /// same name a second time with a different type is an error and not a
335    /// surprise later: the shapes are compared, and a mismatch says which
336    /// field moved and whether the change is additive or breaking.
337    ///
338    /// # Errors
339    ///
340    /// [`Code::ShapeMismatch`] when the name is already a collection of
341    /// another shape.
342    pub fn map<K: Decode, V: Decode>(&self, name: &str) -> Result<Map<K, V>> {
343        let mut desc = Desc::new();
344        desc.map(K::describe, V::describe);
345        let tag = desc.tag();
346
347        let at =
348            self.db.write(
349                |inner| match inner.collections.iter().position(|c| c.name == name) {
350                    Some(at) => {
351                        yo_shape::check(name, &inner.collections[at].desc, &desc, None)?;
352                        Ok(at)
353                    }
354                    None => {
355                        inner.collections.push(Collection {
356                            name: name.to_owned(),
357                            desc,
358                            data: Data::Map(RawMap::new()),
359                        });
360                        Ok(inner.collections.len() - 1)
361                    }
362                },
363            )?;
364        Ok(Map::new(self.db.clone(), at, tag))
365    }
366
367    /// Open a collection of documents, creating it if this is the first time.
368    ///
369    /// `T` is the collection's shape, exactly as it is for [`Db::map`], and it
370    /// also carries the indexes: every field the type marked with `#[yo(index)]`
371    /// or one of its friends is declared here, so a collection cannot be opened
372    /// without the indexes its queries need.
373    ///
374    /// ```
375    /// use yo::Yo;
376    ///
377    /// #[derive(Yo)]
378    /// struct Order {
379    ///     #[yo(id)]
380    ///     id: u64,
381    ///     #[yo(index)]
382    ///     status: String,
383    /// }
384    ///
385    /// let db = yo::open(yo::MEMORY)?;
386    /// let orders = db.docs::<Order>("orders")?;
387    ///
388    /// orders.put(&Order { id: 7, status: "open".to_owned() })?;
389    /// assert_eq!(orders.find(Order::STATUS, "open")?.len(), 1);
390    /// # Ok::<(), yo::Error>(())
391    /// ```
392    ///
393    /// # Errors
394    ///
395    /// [`Code::ShapeMismatch`] when the name is already a collection of another
396    /// shape, and [`Code::Invalid`] for a declared index whose path is not one.
397    pub fn docs<T: Document>(&self, name: &str) -> Result<Docs<T>> {
398        let mut desc = Desc::new();
399        T::describe(&mut desc);
400        let tag = desc.tag();
401
402        let at =
403            self.db.write(
404                |inner| match inner.collections.iter().position(|c| c.name == name) {
405                    Some(at) => {
406                        yo_shape::check(name, &inner.collections[at].desc, &desc, None)?;
407                        // The shape matched, so the indexes are the ones already
408                        // here and declaring them again is nothing at all. This runs
409                        // anyway because it is what will create them on a collection
410                        // read back off disk in M5.
411                        declare::<T>(inner.collections[at].data.docs_mut())?;
412                        Ok(at)
413                    }
414                    None => {
415                        let mut data = Documents::new();
416                        declare::<T>(&mut data)?;
417                        inner.collections.push(Collection {
418                            name: name.to_owned(),
419                            desc,
420                            data: Data::Docs(Box::new(data)),
421                        });
422                        Ok(inner.collections.len() - 1)
423                    }
424                },
425            )?;
426        Ok(Docs::new(self.db.clone(), at, tag))
427    }
428
429    /// Open a graph, creating it if this is the first time.
430    ///
431    /// The name has one shape like every other collection, and it is the same
432    /// shape for every graph, because a graph's types are not fixed when it is
433    /// opened. A node type or an edge type registers itself under its label the
434    /// first time it is used, and the shape it registered is checked on every
435    /// later use, so the check is per label rather than one tuple named up
436    /// front. That way adding a node type to a program is not a schema change
437    /// for the types already in the graph.
438    ///
439    /// ```
440    /// use yo::{Edge, Node, Yo};
441    ///
442    /// #[derive(Yo)]
443    /// struct Person { #[yo(id)] id: u64, name: String }
444    ///
445    /// #[derive(Yo)]
446    /// struct Follows { since: i64 }
447    ///
448    /// impl Node for Person { const LABEL: &'static str = "Person"; }
449    /// impl Edge for Follows {
450    ///     type From = Person;
451    ///     type To = Person;
452    ///     const LABEL: &'static str = "FOLLOWS";
453    /// }
454    ///
455    /// let db = yo::open(yo::MEMORY)?;
456    /// let g = db.graph("social")?;
457    ///
458    /// let ada = g.add(&Person { id: 1, name: "ada".to_owned() })?;
459    /// let grace = g.add(&Person { id: 2, name: "grace".to_owned() })?;
460    /// g.link(ada, grace, &Follows { since: 2026 })?;
461    ///
462    /// assert_eq!(g.out::<Follows>(ada)?, vec![grace]);
463    /// # Ok::<(), yo::Error>(())
464    /// ```
465    ///
466    /// # Errors
467    ///
468    /// [`Code::ShapeMismatch`] when the name is already a collection of another
469    /// shape, which includes a name that is already a map or a document
470    /// collection.
471    pub fn graph(&self, name: &str) -> Result<Graph> {
472        let mut desc = Desc::new();
473        AGraph::describe(&mut desc);
474
475        let at =
476            self.db.write(
477                |inner| match inner.collections.iter().position(|c| c.name == name) {
478                    Some(at) => {
479                        yo_shape::check(name, &inner.collections[at].desc, &desc, None)?;
480                        Ok(at)
481                    }
482                    None => {
483                        inner.collections.push(Collection {
484                            name: name.to_owned(),
485                            desc,
486                            data: Data::Graph(Box::new(crate::graph::Store::new())),
487                        });
488                        Ok(inner.collections.len() - 1)
489                    }
490                },
491            )?;
492        Ok(Graph::new(self.db.clone(), at))
493    }
494
495    /// Open a collection of vectors, creating it if this is the first time.
496    ///
497    /// Nearness is euclidean distance. [`Db::vectors_with`] is the same call
498    /// with the metric spelled out, and cosine is the other one worth having.
499    ///
500    /// The dimension is part of the collection's shape exactly as a map's value
501    /// type is, so a collection opened at 768 dimensions cannot later be opened
502    /// at 1536 and quietly read the old vectors as half of a new one.
503    ///
504    /// ```
505    /// let db = yo::open(yo::MEMORY)?;
506    /// let v = db.vectors("passages", 3)?;
507    ///
508    /// v.put("a", &[1.0, 0.0, 0.0])?;
509    /// v.put("b", &[0.0, 1.0, 0.0])?;
510    ///
511    /// assert_eq!(v.search(&[0.9, 0.1, 0.0], 1)?[0].key, b"a".to_vec());
512    /// # Ok::<(), yo::Error>(())
513    /// ```
514    ///
515    /// # Errors
516    ///
517    /// [`Code::ShapeMismatch`] when the name is already a collection of another
518    /// shape, which includes the same name at another dimension or metric, and
519    /// [`Code::Invalid`] for a dimension no collection can hold.
520    pub fn vectors(&self, name: &str, dim: usize) -> Result<Vectors> {
521        self.vectors_with(name, dim, yo_shape::Metric::L2)
522    }
523
524    /// The same, saying what nearness means.
525    ///
526    /// [`Metric::Cosine`](yo_shape::Metric::Cosine) stores the unit vector and
527    /// reports one minus the cosine similarity, which is what a collection of
528    /// text embeddings wants. See the [`vector`](crate::vector) module for what
529    /// each metric does and why two of the four are refused.
530    ///
531    /// # Errors
532    ///
533    /// As [`Db::vectors`], and [`Code::Unsupported`] for a metric this build
534    /// does not measure.
535    pub fn vectors_with(
536        &self,
537        name: &str,
538        dim: usize,
539        metric: yo_shape::Metric,
540    ) -> Result<Vectors> {
541        let width = yo_vector::collection::width(dim)?;
542        yo_vector::collection::check_metric(metric)?;
543
544        let mut desc = Desc::new();
545        desc.vector(width, metric);
546
547        let at =
548            self.db.write(
549                |inner| match inner.collections.iter().position(|c| c.name == name) {
550                    Some(at) => {
551                        yo_shape::check(name, &inner.collections[at].desc, &desc, None)?;
552                        Ok(at)
553                    }
554                    None => {
555                        inner.collections.push(Collection {
556                            name: name.to_owned(),
557                            desc,
558                            data: Data::Vectors(Box::new(yo_vector::Collection::new(dim, metric)?)),
559                        });
560                        Ok(inner.collections.len() - 1)
561                    }
562                },
563            )?;
564        Ok(Vectors {
565            db: self.db.clone(),
566            at,
567            dim,
568            metric,
569        })
570    }
571
572    /// The Redis string keyspace.
573    ///
574    /// The same store a client reaches over RESP, reached without the socket,
575    /// the parser or the reply (Y23). Not a named collection, because in Redis
576    /// a string is not one: it is the keyspace itself.
577    ///
578    /// ```
579    /// let db = yo::open(yo::MEMORY)?;
580    /// assert_eq!(db.strings().incr("hits")?, 1);
581    /// # Ok::<(), yo::Error>(())
582    /// ```
583    #[must_use]
584    pub fn strings(&self) -> Strings {
585        Strings {
586            db: self.db.clone(),
587        }
588    }
589
590    /// A counter at one key, which is `15` section 2's `db.counter("hits")`.
591    ///
592    /// Sugar over [`Db::strings`] and worth having: a counter is the commonest
593    /// thing a string key is, and a handle that holds the key means the key is
594    /// spelled once rather than at every call site.
595    ///
596    /// ```
597    /// let db = yo::open(yo::MEMORY)?;
598    /// let hits = db.counter("hits");
599    ///
600    /// hits.incr()?;
601    /// hits.add(9)?;
602    /// assert_eq!(hits.get()?, 10);
603    /// # Ok::<(), yo::Error>(())
604    /// ```
605    #[must_use]
606    pub fn counter(&self, key: impl Into<Vec<u8>>) -> Counter {
607        Counter {
608            db: self.db.clone(),
609            key: key.into(),
610        }
611    }
612
613    /// Every Redis set command, with the key as the first argument.
614    ///
615    /// The same store `SADD` off a socket reaches. Like [`Db::strings`] this is
616    /// not a named collection, because in Redis a set is not one: it is a key in
617    /// the keyspace that happens to hold a set.
618    ///
619    /// ```
620    /// let db = yo::open(yo::MEMORY)?;
621    /// db.sets().add_many("online", &["alice", "bob"])?;
622    /// assert_eq!(db.sets().len_of("online")?, 2);
623    /// # Ok::<(), yo::Error>(())
624    /// ```
625    #[must_use]
626    pub fn sets(&self) -> Sets {
627        Sets {
628            db: self.db.clone(),
629        }
630    }
631
632    /// A set at one key, which is the same sugar [`Db::counter`] is.
633    ///
634    /// ```
635    /// let db = yo::open(yo::MEMORY)?;
636    /// let online = db.set("online");
637    ///
638    /// online.add("alice")?;
639    /// assert!(online.contains("alice")?);
640    /// # Ok::<(), yo::Error>(())
641    /// ```
642    #[must_use]
643    pub fn set(&self, key: impl Into<Vec<u8>>) -> Set {
644        Set {
645            sets: self.sets(),
646            key: key.into(),
647        }
648    }
649
650    /// Every command that works on a key whatever the key holds.
651    ///
652    /// `DEL`, `EXISTS` and `TYPE`, and the whole expiry family. These are the
653    /// ones that belong to the keyspace rather than to a type, which is why
654    /// they are not on [`Db::strings`] or [`Db::sets`]: a deadline sits in the
655    /// key's record and does not care what the record points at.
656    ///
657    /// ```
658    /// use std::time::Duration;
659    ///
660    /// let db = yo::open(yo::MEMORY)?;
661    /// db.set("online").add("alice")?;
662    /// db.keys().expire_in("online", Duration::from_secs(60))?;
663    /// # Ok::<(), yo::Error>(())
664    /// ```
665    #[must_use]
666    pub fn keys(&self) -> Keys {
667        Keys {
668            db: self.db.clone(),
669        }
670    }
671
672    /// The names of the typed collections in this database, in the order they
673    /// were first opened.
674    ///
675    /// # Errors
676    ///
677    /// [`Code::Invalid`] if called from inside a callback that is already
678    /// holding this database.
679    pub fn collections(&self) -> Result<Vec<String>> {
680        self.db
681            .read(|inner| Ok(inner.collections.iter().map(|c| c.name.clone()).collect()))
682    }
683
684    /// The shape of a collection, if it exists.
685    ///
686    /// # Errors
687    ///
688    /// [`Code::Invalid`] if called from inside a callback that is already
689    /// holding this database.
690    pub fn shape(&self, name: &str) -> Result<Option<Tag>> {
691        self.db.read(|inner| {
692            Ok(inner
693                .collections
694                .iter()
695                .find(|c| c.name == name)
696                .map(|c| c.desc.tag()))
697        })
698    }
699
700    /// What this database is holding, index and arena together, across the
701    /// keyspace and every typed collection.
702    ///
703    /// # Errors
704    ///
705    /// [`Code::Invalid`] if called from inside a callback that is already
706    /// holding this database.
707    pub fn memory_bytes(&self) -> Result<usize> {
708        self.db.read(|inner| {
709            Ok(inner.strings.memory_bytes()
710                + inner
711                    .collections
712                    .iter()
713                    .map(|c| c.data.memory_bytes())
714                    .sum::<usize>())
715        })
716    }
717
718    /// Whether this database reads the clock on the data path.
719    ///
720    /// False until something is given a deadline, because until then the
721    /// clock's answer cannot change any reply. `04` section 5 is the reason
722    /// this is worth a method: a clock read is tens of nanoseconds against a
723    /// budget of a hundred and fifty.
724    #[must_use]
725    pub fn reads_the_clock(&self) -> bool {
726        self.db.read(|inner| Ok(inner.deadlines)).unwrap_or(false)
727    }
728
729    /// Whether two databases are the same one.
730    #[must_use]
731    pub fn is(&self, other: &Db) -> bool {
732        self.db.is(&other.db)
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739
740    #[test]
741    fn a_path_on_disk_says_which_build_would_take_it() {
742        let e = open("app.yo").expect_err("no file format yet");
743        assert_eq!(e.code(), Code::Unsupported);
744        assert!(e.message().contains("M5"), "{e}");
745    }
746
747    #[test]
748    fn opening_the_same_name_twice_gives_the_same_collection() {
749        let db = open(MEMORY).unwrap();
750        let a = db.map::<String, u64>("hits").unwrap();
751        let b = db.map::<String, u64>("hits").unwrap();
752        a.set("home", &1).unwrap();
753        assert_eq!(b.get("home").unwrap(), Some(1));
754        assert_eq!(db.collections().unwrap(), vec!["hits".to_owned()]);
755    }
756
757    /// The whole reason the tag exists, from the caller's side: the second
758    /// open does not quietly hand back a map that reads other people's bytes
759    /// as its own type.
760    #[test]
761    fn opening_the_same_name_with_another_type_is_a_shape_mismatch() {
762        let db = open(MEMORY).unwrap();
763        let _first = db.map::<String, u64>("hits").unwrap();
764        let e = db
765            .map::<String, String>("hits")
766            .expect_err("that is a different shape");
767        assert_eq!(e.code(), Code::ShapeMismatch);
768        assert!(
769            e.message().contains("the type changed from u64 to str"),
770            "{e}"
771        );
772        assert_eq!(e.detail(), Some("change=breaking"));
773    }
774
775    #[test]
776    fn two_collections_are_two_keyspaces() {
777        let db = open(MEMORY).unwrap();
778        let a = db.map::<String, u64>("a").unwrap();
779        let b = db.map::<String, u64>("b").unwrap();
780        a.set("k", &1).unwrap();
781        b.set("k", &2).unwrap();
782        assert_eq!(a.get("k").unwrap(), Some(1));
783        assert_eq!(b.get("k").unwrap(), Some(2));
784        assert_eq!(db.collections().unwrap().len(), 2);
785    }
786
787    /// A typed collection and the Redis keyspace do not see each other, which
788    /// is what the catalogue in `07` section 5 says: a collection is a name,
789    /// and the string type is the keyspace.
790    #[test]
791    fn a_typed_collection_and_the_keyspace_are_not_the_same_store() {
792        let db = open(MEMORY).unwrap();
793        let map = db.map::<String, u64>("hits").unwrap();
794        map.set("home", &1).unwrap();
795        db.strings().set("home", "elsewhere").unwrap();
796
797        assert_eq!(map.get("home").unwrap(), Some(1));
798        assert_eq!(
799            db.strings().get("home").unwrap().as_deref(),
800            Some(&b"elsewhere"[..])
801        );
802    }
803
804    #[test]
805    fn a_shape_can_be_read_back_and_an_unopened_name_has_none() {
806        let db = open(MEMORY).unwrap();
807        let map = db.map::<String, u64>("hits").unwrap();
808        assert_eq!(db.shape("hits").unwrap(), Some(map.tag()));
809        assert_eq!(db.shape("misses").unwrap(), None);
810    }
811
812    #[test]
813    fn a_clone_is_the_same_database() {
814        let db = open(MEMORY).unwrap();
815        let map = db.map::<String, u64>("hits").unwrap();
816        map.set("home", &3).unwrap();
817        let same = db.clone();
818        assert_eq!(
819            same.map::<String, u64>("hits")
820                .unwrap()
821                .get("home")
822                .unwrap(),
823            Some(3)
824        );
825        assert!(db.is(&same));
826        assert!(!db.is(&open(MEMORY).unwrap()));
827        assert!(db.memory_bytes().unwrap() > 0);
828        assert!(format!("{db:?}").contains("hits"));
829    }
830}