Skip to main content

yo_doc/
lib.rs

1//! The document model: YOJB, the encoding, and the collection it is stored in
2//! (`09` sections 2 and 4).
3//!
4//! A document database is a binary JSON encoding plus secondary indexes over
5//! paths into it. [`Value`] and [`Builder`] are the encoding, which is JSONB in
6//! spirit, which is Postgres and CockroachDB in spirit, with the differences
7//! that matter for an embedded engine written down below. [`Docs`] is the
8//! collection: documents by id, with the [`Keys`] table that turns every object
9//! key into two bytes and a [`PathIndex`] per path that is worth looking
10//! documents up by, for equality, for ranges, for the elements of an array or
11//! for the words of a string.
12//!
13//! ```
14//! use yo_doc::{Builder, Kind, Value};
15//!
16//! let mut b = Builder::new();
17//! b.begin_object()?;
18//! b.key(b"id")?;
19//! b.int(41_920)?;
20//! b.key(b"name")?;
21//! b.text("a wrench")?;
22//! b.key(b"price")?;
23//! b.float(12.5)?;
24//! b.end_object()?;
25//! let doc = b.finish()?.to_vec();
26//!
27//! let v = Value::new(&doc).unwrap();
28//! assert_eq!(v.kind(), Kind::Object);
29//! assert_eq!(v.get(b"name").unwrap().as_text(), Some("a wrench"));
30//! assert_eq!(v.path("$.price")?.unwrap().as_float(), Some(12.5));
31//! # Ok::<(), yo_common::Error>(())
32//! ```
33//!
34//! # The shape of a value
35//!
36//! Every value, at every level, begins with a four byte header: three bits of
37//! kind, a bit that tells an object from an array, four flags, and a
38//! twenty four bit count that is an element count for a container and a payload
39//! length for a scalar. A scalar is the header and its bytes. A container is
40//! the header, an entry table, and then the elements.
41//!
42//! See [`layout`] for the container layout and why each piece is where it is.
43//!
44//! # Three differences from Postgres JSONB
45//!
46//! **Keys are interned per collection.** A typed collection assigns every field
47//! name it has seen a two byte id, and an object written into it stores ids
48//! rather than bytes. Document collections repeat the same twenty field names
49//! on every document, so this is worth roughly forty percent of a collection's
50//! size, and it turns a member lookup from a comparison of bytes into a
51//! comparison of integers. [`Keys`] is the table that hands out the ids and
52//! [`Docs::put`] is what applies it.
53//!
54//! **A container is capped at 16.7 M elements**, because the count shares a
55//! word with the kind and the flags. That is one word of overhead per value
56//! rather than Postgres's per entry scheme with a separate container header.
57//!
58//! **Nothing inside a value is compressed.** Compression is a record level flag
59//! (`06` section 2.1), so a path read never has to decompress a document to
60//! reach one field of it. A document model that stores a compressed blob and
61//! calls the fields indexed is a document model that decompresses on every
62//! read.
63//!
64//! [`query`] is the other half of the path grammar. [`Value::path`] answers one
65//! value and refuses `[*]` and `..` because it has nowhere to put a second
66//! answer, and [`Path`] is what reads those: a descent, a wildcard, a slice and
67//! a union, which is RFC 9535 without its filter selector. The `JSON.*` surface
68//! is written against sets rather than single values, so it needs both.
69//!
70//! [`edit`](mod@edit) is the write side. A path answers a set of places and an edit says
71//! what happens at each of them, which is a replacement, a removal, a key put
72//! into an object or a run of an array spliced. A document is rebuilt rather
73//! than patched, and everything the edit did not name is a memcpy through
74//! [`Builder::embed`], so the cost follows the size of the document and not the
75//! number of changes.
76//!
77//! [`text`] is JSON text in and out. The typed API never touches it, since a
78//! struct is serialized straight into this encoding and read straight back out
79//! of it, but `JSON.SET` arrives with text and `JSON.GET` has to hand text
80//! back, so the whole `JSON.*` surface stands on [`Builder::json`] and
81//! [`Value::to_json`]. The parser takes RFC 8259 and nothing else, for the
82//! reason spelled out there: every convenience a JSON parser adds is a document
83//! that loads here and is refused by a real Redis.
84//!
85//! # What is not here
86//!
87//! The typed `Docs<T>` surface with its derive, which is `15`.
88//!
89//! # What is here now that was not
90//!
91//! [`VectorIndex`] puts an embedding under a path in the same collection the
92//! document is in, so a nearest neighbour search hands back documents and the
93//! filter over their other indexed fields runs inside the scan. See
94//! [`vector`] for why that is not a [`PathIndex`] and why the
95//! filter has to be inside.
96
97#![deny(missing_docs)]
98
99mod build;
100mod docs;
101pub mod edit;
102mod filter;
103mod head;
104mod index;
105mod keys;
106pub mod layout;
107mod path;
108pub mod query;
109mod read;
110pub mod text;
111pub mod vector;
112
113pub use build::Builder;
114pub use docs::{Doc, DocElems, DocMembers, Docs};
115pub use edit::{Edit, edit};
116pub use head::{COUNT_MAX, DEPTH_MAX, Kind};
117pub use index::{IndexKind, KEY_MAX, Key, PathIndex, Ranged, RangedRev};
118pub use keys::{KEYS_MAX, Keys};
119pub use path::{Step, Steps};
120pub use query::{Computed, Path};
121pub use read::{Elems, Members, Value, key_order};
122pub use text::{Format, from_json};
123pub use vector::VectorIndex;
124pub use yo_kv::Cursor;
125
126/// Why a test in this crate names two sizes rather than dividing one.
127///
128/// Miri charges per operation, and the operations here are not one price. A
129/// [`Builder`] call is cheap and a count of them can be cut tenfold without
130/// much thought. A [`Docs::put_bytes`] writes the document, hashes its names
131/// into the key table and offers it to every index that is declared, so a
132/// hundred of them inside a test that then runs a search is a minute. The most
133/// expensive thing in the crate is reading a damaged document: the reader
134/// sweeps in [`read`] touch every accessor on every part of a document, and one
135/// of those walks is around six tenths of a second, which is why the fuzz
136/// budget there is cut from twenty thousand rounds to thirty two while the
137/// truncation sweep next to it is left alone.
138///
139/// Where the count is the claim rather than a way of reaching it, the test
140/// keeps its number and is skipped under Miri instead, and says so where it is
141/// skipped. In this crate those are the two limits that are compile time
142/// constants: [`DEPTH_MAX`], which two tests sit exactly on so that the refusal
143/// is the limit and not something short of it, and [`KEYS_MAX`], which two more
144/// fill so that the table has somewhere to overflow from. Neither has a runtime
145/// knob and a smaller version of either would be testing a limit the code does
146/// not have.
147///
148/// The figures above came off a census taken single threaded. Nextest charges a
149/// test that is queued behind another one for the wait, so a parallel census
150/// reads as much as fourteen times too slow and is no use for deciding any of
151/// this.
152#[cfg(test)]
153mod miri {}