use core::marker::PhantomData;
use core::ops::{Bound, RangeBounds};
use yo_common::{Code, Error, Result};
use yo_shape::{Shape, Tag};
use crate::db::Handle;
pub use yo_doc::{Builder, Doc, IndexKind, Key};
pub trait Field: Shape + Sized {
fn write(&self, b: &mut Builder) -> Result<()>;
fn read(d: Doc<'_>) -> Result<Self>;
fn missing(name: &str) -> Result<Self> {
Err(Error::fmt(
Code::Corrupt,
format_args!(
"this document has no {name}, and the field is not an Option. Either the collection holds something written under another shape, or the field was added without a default"
),
))
}
}
pub trait Query {
fn key(&self, kind: IndexKind) -> Option<Key>;
}
pub trait Asked: Query {
type Ask: Query + ?Sized;
}
macro_rules! asks_for_itself {
($($t:ty),* $(,)?) => {
$(impl Asked for $t {
type Ask = $t;
})*
};
}
asks_for_itself!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool);
impl Asked for String {
type Ask = str;
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a document",
label = "this type has no id",
note = "add `#[derive(Yo)]` to it and mark one field `#[yo(id)]`, which is what a document is stored under"
)]
pub trait Document: Field + Indexed {
type Id: Field + Asked;
fn id(&self) -> &Self::Id;
}
pub trait Indexed {
const INDEXES: &'static [(&'static str, IndexKind)];
const VECTORS: &'static [(&'static str, usize)] = &[];
}
pub struct Path<T, V> {
path: &'static str,
kind: IndexKind,
marker: PhantomData<fn() -> (T, V)>,
}
impl<T, V> Clone for Path<T, V> {
fn clone(&self) -> Path<T, V> {
*self
}
}
impl<T, V> Copy for Path<T, V> {}
impl<T, V> core::fmt::Debug for Path<T, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Path")
.field("path", &self.path)
.field("kind", &self.kind)
.finish()
}
}
impl<T, V> Path<T, V> {
#[must_use]
pub const fn new(path: &'static str, kind: IndexKind) -> Path<T, V> {
Path {
path,
kind,
marker: PhantomData,
}
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path
}
#[must_use]
pub const fn kind(&self) -> IndexKind {
self.kind
}
}
pub struct Ordered<T, V> {
path: Path<T, V>,
}
impl<T, V> Clone for Ordered<T, V> {
fn clone(&self) -> Ordered<T, V> {
*self
}
}
impl<T, V> Copy for Ordered<T, V> {}
impl<T, V> core::fmt::Debug for Ordered<T, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Ordered")
.field("path", &self.path.path)
.finish()
}
}
impl<T, V> Ordered<T, V> {
#[must_use]
pub const fn new(path: &'static str) -> Ordered<T, V> {
Ordered {
path: Path::new(path, IndexKind::Ordered),
}
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path.path
}
}
impl<T, V> From<Ordered<T, V>> for Path<T, V> {
fn from(o: Ordered<T, V>) -> Path<T, V> {
o.path
}
}
pub struct Vector<T> {
path: &'static str,
dim: usize,
marker: PhantomData<fn() -> T>,
}
impl<T> Clone for Vector<T> {
fn clone(&self) -> Vector<T> {
*self
}
}
impl<T> Copy for Vector<T> {}
impl<T> core::fmt::Debug for Vector<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Vector")
.field("path", &self.path)
.field("dim", &self.dim)
.finish()
}
}
impl<T> Vector<T> {
#[must_use]
pub const fn new(path: &'static str, dim: usize) -> Vector<T> {
Vector {
path,
dim,
marker: PhantomData,
}
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path
}
#[must_use]
pub const fn dim(&self) -> usize {
self.dim
}
}
pub struct Docs<T> {
db: Handle,
at: usize,
tag: Tag,
marker: PhantomData<fn() -> T>,
}
impl<T> Clone for Docs<T> {
fn clone(&self) -> Docs<T> {
Docs {
db: self.db.clone(),
at: self.at,
tag: self.tag,
marker: PhantomData,
}
}
}
impl<T> core::fmt::Debug for Docs<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = self
.db
.read(|inner| Ok(inner.collections[self.at].name.clone()))
.unwrap_or_else(|_| "?".to_owned());
f.debug_struct("Docs").field("name", &name).finish()
}
}
impl<T: Document> Docs<T> {
pub(crate) fn new(db: Handle, at: usize, tag: Tag) -> Docs<T> {
Docs {
db,
at,
tag,
marker: PhantomData,
}
}
pub fn name(&self) -> Result<String> {
self.db
.read(|inner| Ok(inner.collections[self.at].name.clone()))
}
#[must_use]
pub fn tag(&self) -> Tag {
self.tag
}
pub fn put(&self, doc: &T) -> Result<bool> {
let id = key_of(doc.id(), IndexKind::Equality, "the id")?;
self.write(|c| {
c.scratch.clear();
Field::write(doc, &mut c.scratch)?;
let bytes = c.scratch.finish()?;
c.docs.put_bytes(id.as_bytes(), bytes)
})
}
pub fn get(&self, id: &<T::Id as Asked>::Ask) -> Result<Option<T>> {
let id = key_of(id, IndexKind::Equality, "the id")?;
self.read(|docs| match docs.get(id.as_bytes()) {
Some(doc) => T::read(doc).map(Some),
None => Ok(None),
})
}
pub fn contains(&self, id: &<T::Id as Asked>::Ask) -> Result<bool> {
let id = key_of(id, IndexKind::Equality, "the id")?;
self.read(|docs| Ok(docs.contains(id.as_bytes())))
}
pub fn remove(&self, id: &<T::Id as Asked>::Ask) -> Result<bool> {
let id = key_of(id, IndexKind::Equality, "the id")?;
self.write(|c| Ok(c.docs.remove(id.as_bytes())))
}
pub fn len(&self) -> Result<usize> {
self.read(|docs| Ok(docs.len()))
}
pub fn is_empty(&self) -> Result<bool> {
self.read(|docs| Ok(docs.is_empty()))
}
pub fn all(&self) -> Result<Vec<T>> {
self.read(|docs| {
let mut out = Vec::with_capacity(docs.len());
for (_, doc) in docs.iter() {
out.push(T::read(doc)?);
}
Ok(out)
})
}
pub fn find<V: Asked>(&self, path: impl Into<Path<T, V>>, value: &V::Ask) -> Result<Vec<T>> {
let path = path.into();
let key = key_of(value, path.kind, path.path)?;
self.read(|docs| {
let mut out = Vec::new();
let mut bad = Ok(());
docs.find(path.path, &key, |_, doc| {
if bad.is_ok() {
match T::read(doc) {
Ok(v) => out.push(v),
Err(e) => bad = Err(e),
}
}
})?;
bad?;
Ok(out)
})
}
pub fn count<V: Asked>(&self, path: impl Into<Path<T, V>>, value: &V::Ask) -> Result<usize> {
let path = path.into();
let key = key_of(value, path.kind, path.path)?;
self.read(|docs| docs.count(path.path, &key))
}
pub fn range<V: Asked, R: RangeBounds<V::Ask>>(
&self,
path: Ordered<T, V>,
range: R,
) -> Result<Vec<T>> {
let path = path.path();
let (lo, hi) = bounds(&range, path)?;
self.read(|docs| {
let mut out = Vec::new();
let mut bad = Ok(());
docs.range(path, as_ref(&lo), as_ref(&hi), |_, doc| {
if bad.is_ok() {
match T::read(doc) {
Ok(v) => out.push(v),
Err(e) => bad = Err(e),
}
}
})?;
bad?;
Ok(out)
})
}
pub fn range_rev<V: Asked, R: RangeBounds<V::Ask>>(
&self,
path: Ordered<T, V>,
range: R,
) -> Result<Vec<T>> {
let path = path.path();
let (lo, hi) = bounds(&range, path)?;
self.read(|docs| {
let mut out = Vec::new();
let mut bad = Ok(());
docs.range_rev(path, as_ref(&lo), as_ref(&hi), |_, doc| {
if bad.is_ok() {
match T::read(doc) {
Ok(v) => out.push(v),
Err(e) => bad = Err(e),
}
}
})?;
bad?;
Ok(out)
})
}
pub fn count_range<V: Asked, R: RangeBounds<V::Ask>>(
&self,
path: Ordered<T, V>,
range: R,
) -> Result<usize> {
let path = path.path();
let (lo, hi) = bounds(&range, path)?;
self.read(|docs| docs.count_range(path, as_ref(&lo), as_ref(&hi)))
}
pub fn nearest(&self, path: Vector<T>, q: &[f32], k: usize) -> Result<Vec<T>> {
self.near(path, q).take(k)
}
pub fn nearest_to(
&self,
path: Vector<T>,
id: &<T::Id as Asked>::Ask,
k: usize,
) -> Result<Vec<T>> {
let id = key_of(id, IndexKind::Equality, "the id")?;
self.read(|docs| {
let mut out = Vec::new();
let mut bad = Ok(());
docs.nearest_to(path.path(), id.as_bytes(), k, |_, doc, _| {
collect::<T>(&mut out, &mut bad, doc);
})?;
bad?;
Ok(out)
})
}
pub fn near<'a>(&'a self, path: Vector<T>, q: &'a [f32]) -> Near<'a, T> {
Near {
docs: self,
path,
q,
want: Vec::new(),
bad: None,
}
}
pub fn memory_bytes(&self) -> Result<usize> {
self.read(|docs| Ok(docs.memory_bytes()))
}
fn read<R>(&self, f: impl FnOnce(&yo_doc::Docs) -> Result<R>) -> Result<R> {
self.db
.read(|inner| f(inner.collections[self.at].data.docs()))
}
fn write<R>(&self, f: impl FnOnce(&mut Documents) -> Result<R>) -> Result<R> {
self.db
.write(|inner| f(inner.collections[self.at].data.docs_mut()))
}
}
pub struct Near<'a, T> {
docs: &'a Docs<T>,
path: Vector<T>,
q: &'a [f32],
want: Vec<(&'static str, Key)>,
bad: Option<Error>,
}
impl<T> core::fmt::Debug for Near<'_, T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Near")
.field("path", &self.path.path())
.field("filters", &self.want.len())
.finish()
}
}
impl<'a, T: Document> Near<'a, T> {
#[must_use]
pub fn filter<V: Asked>(mut self, path: impl Into<Path<T, V>>, value: &V::Ask) -> Near<'a, T> {
let path = path.into();
match key_of(value, path.kind, path.path) {
Ok(key) => self.want.push((path.path, key)),
Err(e) => self.bad = self.bad.or(Some(e)),
}
self
}
pub fn take(self, k: usize) -> Result<Vec<T>> {
Ok(self.scored(k)?.into_iter().map(|(doc, _)| doc).collect())
}
pub fn scored(self, k: usize) -> Result<Vec<(T, f32)>> {
if let Some(e) = self.bad {
return Err(e);
}
let (path, q, want) = (self.path.path(), self.q, self.want);
self.docs.read(|docs| {
let mut out = Vec::new();
let mut bad = Ok(());
docs.nearest_where(path, q, k, &want, |_, doc, at| {
if bad.is_ok() {
match T::read(doc) {
Ok(v) => out.push((v, at)),
Err(e) => bad = Err(e),
}
}
})?;
bad?;
Ok(out)
})
}
}
fn collect<T: Document>(out: &mut Vec<T>, bad: &mut Result<()>, doc: Doc<'_>) {
if bad.is_ok() {
match T::read(doc) {
Ok(v) => out.push(v),
Err(e) => *bad = Err(e),
}
}
}
pub(crate) struct Documents {
pub(crate) docs: yo_doc::Docs,
pub(crate) scratch: Builder,
}
impl Documents {
pub(crate) fn new() -> Documents {
Documents {
docs: yo_doc::Docs::new(),
scratch: Builder::new(),
}
}
}
pub(crate) fn key_of<Q: Query + ?Sized>(value: &Q, kind: IndexKind, what: &str) -> Result<Key> {
let key = value.key(kind).ok_or_else(|| {
let why = if kind == IndexKind::Text {
"a text index holds one word at a time, and this is not one word"
} else {
"an index does not file this type, so it cannot be looked up"
};
Error::fmt(Code::Invalid, format_args!("{what}: {why}"))
})?;
if key.is_too_long() {
return Err(Error::fmt(
Code::Full,
format_args!(
"{what} is longer than {} bytes, which is as long as a key can be",
yo_doc::KEY_MAX
),
));
}
Ok(key)
}
fn bounds<Q, R>(range: &R, path: &str) -> Result<(Bound<Key>, Bound<Key>)>
where
Q: Query + ?Sized,
R: RangeBounds<Q>,
{
Ok((
one(range.start_bound(), path)?,
one(range.end_bound(), path)?,
))
}
fn one<Q: Query + ?Sized>(b: Bound<&Q>, path: &str) -> Result<Bound<Key>> {
Ok(match b {
Bound::Included(v) => Bound::Included(key_of(v, IndexKind::Ordered, path)?),
Bound::Excluded(v) => Bound::Excluded(key_of(v, IndexKind::Ordered, path)?),
Bound::Unbounded => Bound::Unbounded,
})
}
fn as_ref(b: &Bound<Key>) -> Bound<&Key> {
match b {
Bound::Included(k) => Bound::Included(k),
Bound::Excluded(k) => Bound::Excluded(k),
Bound::Unbounded => Bound::Unbounded,
}
}
pub fn at<V: Field>(d: Doc<'_>, name: &str) -> Result<V> {
match d.get(name.as_bytes()) {
Some(at) => V::read(at),
None => V::missing(name),
}
}
pub fn expect_object(d: Doc<'_>, name: &str) -> Result<()> {
if d.kind() == yo_doc::Kind::Object {
return Ok(());
}
Err(Error::fmt(
Code::Corrupt,
format_args!("a {name} in this collection is stored as {:?}", d.kind()),
))
}
fn not_a(want: &str, d: Doc<'_>) -> Error {
Error::fmt(
Code::Corrupt,
format_args!(
"this field should be a {want} and is stored as {:?}",
d.kind()
),
)
}
macro_rules! ints {
($($t:ty),* $(,)?) => {
$(
impl Field for $t {
fn write(&self, b: &mut Builder) -> Result<()> {
b.int(i64::from(*self))
}
fn read(d: Doc<'_>) -> Result<$t> {
let n = d.as_int().ok_or_else(|| not_a(stringify!($t), d))?;
<$t>::try_from(n).map_err(|_| {
Error::fmt(
Code::Corrupt,
format_args!("{n} does not fit in a {}", stringify!($t)),
)
})
}
}
impl Query for $t {
fn key(&self, _kind: IndexKind) -> Option<Key> {
Some(Key::int(i64::from(*self)))
}
}
)*
};
}
ints!(i8, i16, i32, i64, u8, u16, u32);
impl Field for u64 {
fn write(&self, b: &mut Builder) -> Result<()> {
match i64::try_from(*self) {
Ok(n) => b.int(n),
Err(_) => Err(Error::fmt(
Code::Invalid,
format_args!(
"{self} is past i64::MAX, and a document holds one number type, which is signed"
),
)),
}
}
fn read(d: Doc<'_>) -> Result<u64> {
let n = d.as_int().ok_or_else(|| not_a("u64", d))?;
u64::try_from(n).map_err(|_| {
Error::fmt(
Code::Corrupt,
format_args!("{n} is negative and this field is a u64"),
)
})
}
}
impl Query for u64 {
fn key(&self, _kind: IndexKind) -> Option<Key> {
i64::try_from(*self).ok().map(Key::int)
}
}
macro_rules! floats {
($($t:ty),* $(,)?) => {
$(
impl Field for $t {
fn write(&self, b: &mut Builder) -> Result<()> {
b.float(f64::from(*self))
}
fn read(d: Doc<'_>) -> Result<$t> {
match (d.as_float(), d.as_int()) {
(Some(v), _) => Ok(v as $t),
(None, Some(n)) => Ok(n as $t),
(None, None) => Err(not_a(stringify!($t), d)),
}
}
}
impl Query for $t {
fn key(&self, _kind: IndexKind) -> Option<Key> {
Some(Key::float(f64::from(*self)))
}
}
)*
};
}
floats!(f32, f64);
impl Field for bool {
fn write(&self, b: &mut Builder) -> Result<()> {
b.bool(*self)
}
fn read(d: Doc<'_>) -> Result<bool> {
d.as_bool().ok_or_else(|| not_a("bool", d))
}
}
impl Query for bool {
fn key(&self, _kind: IndexKind) -> Option<Key> {
Some(Key::bool(*self))
}
}
impl Field for String {
fn write(&self, b: &mut Builder) -> Result<()> {
b.text(self)
}
fn read(d: Doc<'_>) -> Result<String> {
d.as_text()
.map(str::to_owned)
.ok_or_else(|| not_a("string", d))
}
}
impl Query for String {
fn key(&self, kind: IndexKind) -> Option<Key> {
self.as_str().key(kind)
}
}
impl Query for str {
fn key(&self, kind: IndexKind) -> Option<Key> {
match kind {
IndexKind::Text => Key::word(self),
_ => Some(Key::text(self)),
}
}
}
impl<T: Field> Field for Option<T> {
fn write(&self, b: &mut Builder) -> Result<()> {
match self {
Some(v) => v.write(b),
None => b.null(),
}
}
fn read(d: Doc<'_>) -> Result<Option<T>> {
if d.is_null() {
return Ok(None);
}
T::read(d).map(Some)
}
fn missing(_name: &str) -> Result<Option<T>> {
Ok(None)
}
}
impl<T: Field> Field for Vec<T> {
fn write(&self, b: &mut Builder) -> Result<()> {
b.begin_array()?;
for v in self {
v.write(b)?;
}
b.end_array()
}
fn read(d: Doc<'_>) -> Result<Vec<T>> {
if d.kind() != yo_doc::Kind::Array {
return Err(not_a("list", d));
}
let mut out = Vec::with_capacity(d.len());
for elem in d.iter() {
out.push(T::read(elem)?);
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Yo, open};
#[derive(Yo, Debug, Clone, PartialEq)]
struct Order {
#[yo(id)]
id: u64,
#[yo(index)]
status: String,
#[yo(ordered)]
total: f64,
#[yo(array)]
tags: Vec<String>,
#[yo(text)]
note: String,
sent: Option<String>,
}
fn order(id: u64, status: &str, total: f64) -> Order {
Order {
id,
status: status.to_owned(),
total,
tags: Vec::new(),
note: String::new(),
sent: None,
}
}
fn three() -> (crate::Db, Docs<Order>) {
let db = open(crate::MEMORY).expect("a database in memory");
let orders = db.docs::<Order>("orders").expect("a new collection");
for o in [
order(1, "open", 12.5),
order(2, "shipped", 99.0),
order(3, "open", 40.0),
] {
orders.put(&o).expect("a document that fits");
}
(db, orders)
}
#[test]
fn a_document_comes_back_as_the_struct_that_went_in() {
let (_db, orders) = three();
assert_eq!(
orders.get(&1).expect("a read"),
Some(order(1, "open", 12.5))
);
assert_eq!(orders.get(&9).expect("a read"), None);
assert_eq!(orders.len().expect("a count"), 3);
assert!(orders.contains(&2).expect("a read"));
}
#[test]
fn every_field_kind_survives_the_round_trip() {
let db = open(crate::MEMORY).expect("a database in memory");
let orders = db.docs::<Order>("orders").expect("a new collection");
let o = Order {
id: 7,
status: "open".to_owned(),
total: -0.5,
tags: vec!["red".to_owned(), "small".to_owned()],
note: "A red kite".to_owned(),
sent: Some("tuesday".to_owned()),
};
orders.put(&o).expect("a document that fits");
assert_eq!(orders.get(&7).expect("a read"), Some(o));
}
#[test]
fn putting_the_same_id_twice_replaces_it() {
let (_db, orders) = three();
assert!(!orders.put(&order(1, "shut", 1.0)).expect("a write"));
assert_eq!(orders.len().expect("a count"), 3);
assert_eq!(
orders.get(&1).expect("a read").expect("it is there").status,
"shut"
);
assert_eq!(orders.count(Order::STATUS, "open").expect("a count"), 1);
}
#[test]
fn removing_a_document_takes_it_out_of_its_indexes() {
let (_db, orders) = three();
assert!(orders.remove(&1).expect("a write"));
assert!(!orders.remove(&1).expect("a write"));
assert_eq!(orders.len().expect("a count"), 2);
assert_eq!(orders.count(Order::STATUS, "open").expect("a count"), 1);
assert!(orders.find(Order::TOTAL, &12.5).expect("a read").is_empty());
}
#[test]
fn an_equality_index_answers_with_the_documents() {
let (_db, orders) = three();
let mut open = orders.find(Order::STATUS, "open").expect("a read");
open.sort_by_key(|o| o.id);
assert_eq!(open, [order(1, "open", 12.5), order(3, "open", 40.0)]);
assert_eq!(orders.count(Order::STATUS, "gone").expect("a count"), 0);
}
#[test]
fn a_range_over_a_float_field_is_in_numeric_order() {
let (_db, orders) = three();
let cheap = orders.range(Order::TOTAL, 0.0..50.0).expect("a read");
assert_eq!(
cheap.iter().map(|o| o.total).collect::<Vec<_>>(),
[12.5, 40.0]
);
let all = orders.range(Order::TOTAL, ..).expect("a read");
assert_eq!(
all.iter().map(|o| o.total).collect::<Vec<_>>(),
[12.5, 40.0, 99.0]
);
let down = orders.range_rev(Order::TOTAL, ..).expect("a read");
assert_eq!(
down.iter().map(|o| o.total).collect::<Vec<_>>(),
[99.0, 40.0, 12.5]
);
assert_eq!(
orders
.count_range(Order::TOTAL, 12.5..=40.0)
.expect("a count"),
2
);
}
#[test]
fn an_ordered_path_can_still_be_asked_for_equality() {
let (_db, orders) = three();
assert_eq!(orders.find(Order::TOTAL, &40.0).expect("a read").len(), 1);
assert_eq!(orders.count(Order::TOTAL, &99.0).expect("a count"), 1);
}
#[test]
fn a_range_over_a_string_field_takes_a_pair_of_bounds() {
let db = open(crate::MEMORY).expect("a database in memory");
let names = db.docs::<Named>("names").expect("a new collection");
for (id, name) in [(1u64, "banana"), (2, "apple"), (3, "quince")] {
names
.put(&Named {
id,
name: name.to_owned(),
})
.expect("a document that fits");
}
let early = names
.range(Named::NAME, (Bound::Included("a"), Bound::Excluded("m")))
.expect("a read");
assert_eq!(
early.iter().map(|n| n.name.as_str()).collect::<Vec<_>>(),
["apple", "banana"]
);
}
#[derive(Yo, Debug, PartialEq)]
struct Named {
#[yo(id)]
id: u64,
#[yo(ordered)]
name: String,
}
#[test]
fn an_array_index_files_a_document_under_every_element() {
let db = open(crate::MEMORY).expect("a database in memory");
let orders = db.docs::<Order>("orders").expect("a new collection");
let mut o = order(1, "open", 1.0);
o.tags = vec!["red".to_owned(), "small".to_owned()];
orders.put(&o).expect("a document that fits");
assert_eq!(orders.find(Order::TAGS, "red").expect("a read").len(), 1);
assert_eq!(orders.find(Order::TAGS, "small").expect("a read").len(), 1);
assert_eq!(orders.count(Order::TAGS, "large").expect("a count"), 0);
}
#[test]
fn a_text_index_files_a_document_under_every_word() {
let db = open(crate::MEMORY).expect("a database in memory");
let orders = db.docs::<Order>("orders").expect("a new collection");
let mut o = order(1, "open", 1.0);
o.note = "A red kite".to_owned();
orders.put(&o).expect("a document that fits");
assert_eq!(orders.find(Order::NOTE, "RED").expect("a read").len(), 1);
assert_eq!(orders.find(Order::NOTE, "kite").expect("a read").len(), 1);
assert_eq!(orders.count(Order::NOTE, "blue").expect("a count"), 0);
}
#[test]
fn asking_a_text_index_for_a_phrase_says_so() {
let (_db, orders) = three();
let e = orders
.find(Order::NOTE, "red kite")
.expect_err("not one word");
assert_eq!(e.code(), crate::Code::Invalid);
assert!(e.message().contains("one word"), "{}", e.message());
}
#[test]
fn an_absent_field_reads_back_as_none() {
let (_db, orders) = three();
assert_eq!(
orders.get(&1).expect("a read").expect("it is there").sent,
None
);
}
#[test]
fn a_nested_struct_is_a_field() {
#[derive(Yo, Debug, PartialEq)]
struct Where {
city: String,
postcode: String,
}
#[derive(Yo, Debug, PartialEq)]
struct Person {
#[yo(id)]
id: u64,
home: Where,
}
let db = open(crate::MEMORY).expect("a database in memory");
let people = db.docs::<Person>("people").expect("a new collection");
let p = Person {
id: 1,
home: Where {
city: "Hanoi".to_owned(),
postcode: "100000".to_owned(),
},
};
people.put(&p).expect("a document that fits");
assert_eq!(people.get(&1).expect("a read"), Some(p));
}
#[test]
fn all_walks_every_document() {
let (_db, orders) = three();
let mut ids: Vec<u64> = orders.all().expect("a read").iter().map(|o| o.id).collect();
ids.sort_unstable();
assert_eq!(ids, [1, 2, 3]);
}
#[test]
fn opening_a_collection_as_the_wrong_thing_is_refused() {
let db = open(crate::MEMORY).expect("a database in memory");
let _orders = db.docs::<Order>("orders").expect("a new collection");
let e = db
.map::<String, u64>("orders")
.expect_err("a different shape");
assert_eq!(e.code(), crate::Code::ShapeMismatch);
let e = db.docs::<Named>("orders").expect_err("a different struct");
assert_eq!(e.code(), crate::Code::ShapeMismatch);
}
#[test]
fn reopening_a_collection_hands_back_the_same_documents() {
let (db, orders) = three();
let again = db.docs::<Order>("orders").expect("the same collection");
assert_eq!(again.len().expect("a count"), 3);
assert_eq!(again.count(Order::STATUS, "open").expect("a count"), 2);
drop(orders);
}
#[test]
fn a_u64_past_what_json_can_hold_is_refused() {
let db = open(crate::MEMORY).expect("a database in memory");
let orders = db.docs::<Order>("orders").expect("a new collection");
let e = orders
.put(&order(u64::MAX, "open", 1.0))
.expect_err("too big");
assert_eq!(e.code(), crate::Code::Invalid);
}
#[derive(Yo, Debug, Clone, PartialEq)]
struct Note {
#[yo(id)]
id: u64,
#[yo(index)]
lang: String,
#[yo(vector = 4)]
embedding: Vec<f32>,
}
fn note(id: u64, lang: &str, embedding: [f32; 4]) -> Note {
Note {
id,
lang: lang.to_owned(),
embedding: embedding.to_vec(),
}
}
fn notes() -> (crate::Db, Docs<Note>) {
let db = open(crate::MEMORY).expect("a database in memory");
let notes = db.docs::<Note>("notes").expect("a new collection");
for n in [
note(1, "en", [1.0, 0.0, 0.0, 0.0]),
note(2, "fr", [0.0, 1.0, 0.0, 0.0]),
note(3, "en", [0.0, 0.0, 1.0, 0.0]),
note(4, "fr", [0.0, 0.0, 0.0, 1.0]),
] {
notes.put(&n).expect("a document that fits");
}
(db, notes)
}
#[test]
fn a_derived_vector_field_is_indexed_and_comes_back_whole() {
let (_db, notes) = notes();
assert_eq!(
Note::VECTORS,
[("$.embedding", 4usize)],
"the derive declares the path and the width"
);
assert_eq!(Note::EMBEDDING.path(), "$.embedding");
assert_eq!(Note::EMBEDDING.dim(), 4);
assert_eq!(
notes.get(&2).expect("a read"),
Some(note(2, "fr", [0.0, 1.0, 0.0, 0.0]))
);
let near = notes
.nearest(Note::EMBEDDING, &[0.9, 0.1, 0.0, 0.0], 2)
.expect("a search");
assert_eq!(near.iter().map(|n| n.id).collect::<Vec<_>>(), [1, 2]);
}
#[test]
fn a_filter_narrows_the_search_and_not_the_answers() {
let (_db, notes) = notes();
let french = notes
.near(Note::EMBEDDING, &[0.9, 0.1, 0.0, 0.0])
.filter(Note::LANG, "fr")
.take(2)
.expect("a search");
assert_eq!(french.iter().map(|n| n.id).collect::<Vec<_>>(), [2, 4]);
let scored = notes
.near(Note::EMBEDDING, &[0.9, 0.1, 0.0, 0.0])
.filter(Note::LANG, "fr")
.scored(2)
.expect("a search");
assert_eq!(scored[0].0.id, 2);
assert!(scored[0].1 <= scored[1].1);
let e = notes
.near(Note::EMBEDDING, &[1.0, 0.0, 0.0, 0.0])
.filter(
Path::<Note, String>::new("$.author", IndexKind::Equality),
"me",
)
.take(1)
.expect_err("no index there");
assert_eq!(e.code(), crate::Code::Invalid);
}
#[test]
fn more_like_this_leaves_the_document_itself_out() {
let (_db, notes) = notes();
let like = notes.nearest_to(Note::EMBEDDING, &1, 2).expect("a search");
assert_eq!(like.len(), 2);
assert!(!like.iter().any(|n| n.id == 1));
assert!(
notes
.nearest_to(Note::EMBEDDING, &99, 2)
.expect("a search")
.is_empty()
);
}
#[test]
fn an_embedding_of_the_wrong_width_is_refused() {
let db = open(crate::MEMORY).expect("a database in memory");
let notes = db.docs::<Note>("notes").expect("a new collection");
let e = notes
.put(&Note {
id: 1,
lang: "en".to_owned(),
embedding: vec![1.0, 0.0],
})
.expect_err("two coordinates where four were declared");
assert_eq!(e.code(), crate::Code::Invalid);
assert_eq!(notes.len().expect("a count"), 0);
let e = notes
.nearest(Note::EMBEDDING, &[1.0, 0.0], 1)
.expect_err("two coordinates");
assert_eq!(e.code(), crate::Code::Invalid);
}
#[test]
fn reopening_a_collection_keeps_the_vector_index() {
let (db, notes) = notes();
let again = db.docs::<Note>("notes").expect("the same collection");
assert_eq!(
again
.nearest(Note::EMBEDDING, &[0.0, 0.0, 0.9, 0.1], 1)
.expect("a search")
.first()
.map(|n| n.id),
Some(3)
);
drop(notes);
}
}