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}