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