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_mut().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_mut().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}