Crates.io | retriever |
lib.rs | retriever |
version | 0.0.3 |
source | src |
created_at | 2019-11-21 20:45:24.275526 |
updated_at | 2019-11-25 18:08:07.819586 |
description | Retriever is an embedded in-memory data store for rust applications. |
homepage | https://www.github.com/itsybitesyspider/retriever |
repository | https://www.github.com/itsybitesyspider/retriever |
max_upload_size | |
id | 183295 |
size | 269,607 |
Retriever is an embedded, in-memory, document-oriented data store for rust applications. It stores ordinary rust data types in a similar manner as a NoSQL database.
Retriever is ideal when you need to index a collection by multiple properties, you need a variety of relations between elements in a collection, or or you need to maintain summary statistics about a collection.
(Image of Callie, a golden retriever puppy, by Wikimedia Commons user MichaelMcPhee.)
Storage::raw()
for an example.use retriever::prelude::*;
use std::borrow::Cow;
use chrono::prelude::*; // Using rust's Chrono crate to handle date/time
// (just for this example, you don't need it)
use std::collections::HashSet;
// This example is going to be about a puppy rescue agency
struct Puppy {
name: String,
rescued_date: Date<Utc>,
adopted_date: Option<Date<Utc>>,
breed: HashSet<String>,
parents: HashSet<Id<i32,String>>,
}
// Some convenience functions for describing puppies
impl Puppy {
fn new(name: &str, rescued_date: Date<Utc>) -> Puppy {
Puppy {
name: String::from(name),
rescued_date,
adopted_date: None,
breed: HashSet::default(),
parents: HashSet::default(),
}
}
fn with_adopted_date(mut self, adopted_date: Date<Utc>) -> Puppy {
self.adopted_date = Some(adopted_date);
self
}
fn with_breeds(mut self, breeds: &[&str]) -> Puppy {
self.breed.extend(breeds.iter().map(|breed| String::from(*breed)));
self
}
fn with_parent(mut self, year: i32, name: &str) -> Puppy {
self.parents.insert(ID.chunk(year).item(String::from(name)));
self
}
}
// We need to implement Record for our Puppy type.
// We choose the year the puppy was rescued as the chunk key,
// and the name of the puppy as the item key.
// Because of this design, we can never have two puppies with same name
// rescued in the same year. They would have the same Id.
impl Record<i32,str> for Puppy {
fn chunk_key(&self) -> Cow<i32> {
Cow::Owned(self.rescued_date.year())
}
fn item_key(&self) -> Cow<str> {
Cow::Borrowed(&self.name)
}
}
// Let's create a storage of puppies.
let mut storage : Storage<i32,str,Puppy> = Storage::new();
// Add some example puppies to work with
storage.add(
Puppy::new("Lucky", Utc.ymd(2019, 3, 27))
.with_adopted_date(Utc.ymd(2019, 9, 13))
.with_breeds(&["beagle"])
);
storage.add(
Puppy::new("Spot", Utc.ymd(2019, 1, 9))
.with_breeds(&["labrador", "dalmation"]) // See below for correct spelling.
.with_parent(2010, "Yeller")
);
storage.add(
Puppy::new("JoJo", Utc.ymd(2018, 9, 2))
.with_adopted_date(Utc.ymd(2019, 5, 1))
.with_breeds(&["labrador","shepherd"])
.with_parent(2010, "Yeller")
);
storage.add(
Puppy::new("Yeller", Utc.ymd(2010, 8, 30))
.with_adopted_date(Utc.ymd(2013, 12, 24))
.with_breeds(&["labrador"])
);
// Get all puppies rescued in 2019:
let q = Chunks([2019]);
let mut rescued_2019 : Vec<_> = storage.query(&q)
.map(|puppy: &Puppy| &puppy.name).collect();
rescued_2019.sort(); // can't depend on iteration order!
assert_eq!(vec!["Lucky","Spot"], rescued_2019);
// Get all puppies rescued in the last 3 years:
let q = Chunks(2017..=2019);
let mut rescued_recently : Vec<_> = storage.query(&q)
.map(|puppy: &Puppy| &puppy.name).collect();
rescued_recently.sort();
assert_eq!(vec!["JoJo","Lucky","Spot"], rescued_recently);
// Get all puppies rescued in march:
let q = Everything.filter(|puppy: &Puppy| puppy.rescued_date.month() == 3);
let mut rescued_in_march : Vec<_> = storage.query(&q)
.map(|puppy| &puppy.name).collect();
rescued_in_march.sort();
assert_eq!(vec!["Lucky"], rescued_in_march);
// Fix spelling of "dalmatian" on all puppies:
let q = Everything.filter(|puppy : &Puppy| puppy.breed.contains("dalmation"));
storage.modify(&q, |mut editor| {
let puppy = editor.get_mut();
puppy.breed.remove("dalmation");
puppy.breed.insert(String::from("dalmatian"));
});
assert_eq!(0, storage.iter().filter(|x| x.breed.contains("dalmation")).count());
assert_eq!(1, storage.iter().filter(|x| x.breed.contains("dalmatian")).count());
// Set up an index of puppies by their parent.
// In SecondaryIndexes, we always return a collection of secondary keys.
// (In this case, a HashSet containing the Ids of the parents.)
let mut by_parents = SecondaryIndex::new(&storage,
|puppy: &Puppy| Cow::Borrowed(&puppy.parents));
// Use an index to search for all children of Yeller:
let yeller_id = ID.chunk(2010).item(String::from("Yeller"));
let q = Everything.matching(&mut by_parents, Cow::Borrowed(&yeller_id));
let mut children_of_yeller : Vec<_> = storage.query(&q)
.map(|puppy: &Puppy| &puppy.name).collect();
children_of_yeller.sort();
assert_eq!(vec!["JoJo","Spot"], children_of_yeller);
// Remove puppies who have been adopted more than five years ago.
let q = Chunks(0..2014).filter(|puppy: &Puppy|
puppy.adopted_date.map(|date| date.year() <= 2014).unwrap_or(false));
assert!(storage.get(&yeller_id).is_some());
storage.remove(&q, std::mem::drop);
assert!(storage.get(&yeller_id).is_none());
Unlike most databases, retriever stores your data as a plain old rust data type inside heap memory. (Specifically, each chunk has a Vec that stores all of the data for that chunk.) It doesn't support access over a network from multiple clients.
Like a traditional database, retriever has a flexible indexing and query system and can model many-to-many relationships between records.
Retriever can be used as a serviceable component store, because records that share the same keys are easy to cross-reference with each other. But Retriever is not designed specifically for game projects, and it tries to balance programmer comfort with reliability and performance.
ECSs use low-cardinality indexes to do an enormous amount of work very quickly. Retriever uses high-cardinality indexes to avoid as much work as possible.
If you know you need to use Data Oriented Design then you might consider an ECS like specs or legion.
Clone + Debug + Eq + Hash + Ord
. See ValidKey
.Storage::new()
.Storage::add()
, Storage::iter()
, Storage::query()
, Storage::modify()
, and
Storage::remove()
to implement CRUD operations on your storage.SecondaryIndex::new()
. Define
secondary indexes by writing a single closure that maps records into zero or more secondary
keys.Reduction::new()
. Define reductions by writing
two closures: (1) A map from the record to a summary, and (2) a fold
of several summaries into a single summary.
Use Reduction::reduce()
to reduce an entire storage to a single summary, or
Reduction::reduce_chunk()
to reduce a single chunk to a single summary.Reduction
on only part of your storage, then that part must be defined
as a single chunk. In the future, I want to implement convolutional reductions that map onto
zero or more chunks.Retriever makes heavy use of Cow
to represent various kinds of index keys. Using Cow
allows retriever to bridge a wide
range of use cases.
A Cow<T>
is usually either Cow::Owned(T)
or Cow::Borrowed(&T)
. The generic parameter refers
to the borrowed form, so Cow<str>
is either Cow::Owned(String)
or Cow::Borrowed(&str)
.
Whenever you see a generic parameter like ChunkKey
, ItemKey
, or IndexKey
,
these keys should also be borrowed forms.
These are good:
Record<i64,str>
Record<i64,&'static str>
Record<i64,Arc<String>>
This will work for the most part but it's weird:
Record<i64,String>
Retriever is licensed under your choice of either the ISC license (a permissive license) or the AGPL v3.0 or later (a strong copyleft license).
The photograph of the puppy is by Wikimedia Commons user MichaelMcPhee. Creative Commons Attribution 3.0 Unported. (Source)
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in retriever by you, shall be licensed as ISC OR AGPL-3.0-or-later, without any additional terms or conditions.
At this stage, any bug reports or questions about unclear documentation are highly valued. Please be patient if I'm not able to respond immediately. I'm also interested in any suggestions that would help further simplify the code base.
License: ISC OR GPL-3.0-or-later