use failure::{err_msg, Error};
use rocksdb::{ColumnFamily, DBIterator, Direction, IteratorMode, Options, DB};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt;
use std::path::{Path, PathBuf};
pub mod base32;
pub trait Document: Sized {
fn from_bytes(key: &[u8], value: &[u8]) -> Result<Self, Error>;
fn to_bytes(&self) -> Result<Vec<u8>, Error>;
fn map(&self, view: &str, emitter: &Emitter) -> Result<(), Error>;
}
pub type ByteMapper = Box<dyn Fn(&[u8], &[u8], &str, &Emitter) -> Result<(), Error> + Send + Sync>;
pub struct Emitter<'a> {
db: &'a DB,
key: &'a [u8],
cf: &'a ColumnFamily,
sep: &'a [u8],
seq: Vec<u8>,
}
impl<'a> Emitter<'a> {
fn new(db: &'a DB, key: &'a [u8], cf: &'a ColumnFamily, sep: &'a [u8]) -> Self {
let seq = db.latest_sequence_number().to_le_bytes().to_vec();
Self {
db,
key,
cf,
sep,
seq,
}
}
pub fn emit<B>(&self, key: B, value: Option<B>) -> Result<(), Error>
where
B: AsRef<[u8]>,
{
let len = key.as_ref().len() + self.sep.len() + self.key.len();
let mut uniq_key: Vec<u8> = Vec::with_capacity(len);
uniq_key.extend_from_slice(&key.as_ref()[..]);
uniq_key.extend_from_slice(self.sep);
uniq_key.extend_from_slice(self.key);
let value_len = value.as_ref().map_or(0, |v| v.as_ref().len());
let mut ivalue: Vec<u8> = Vec::with_capacity(self.seq.len() + value_len);
ivalue.extend_from_slice(&self.seq);
if let Some(value) = value.as_ref() {
ivalue.extend_from_slice(value.as_ref());
}
self.db.put_cf(self.cf, &uniq_key, ivalue)?;
Ok(())
}
}
pub struct Database {
db: DB,
db_path: PathBuf,
views: Vec<String>,
mapper: ByteMapper,
key_sep: Vec<u8>,
}
const VIEW_PREFIX: &str = "mrview-";
const CHANGES_CF: &str = "mrview--changes";
impl Database {
pub fn open_default<I, N>(db_path: &Path, views: I, mapper: ByteMapper) -> Result<Self, Error>
where
I: IntoIterator<Item = N>,
N: ToString,
{
let mut opts = Options::default();
opts.create_if_missing(true);
Database::open(db_path, views, mapper, opts)
}
pub fn open<I, N>(
db_path: &Path,
views: I,
mapper: ByteMapper,
opts: Options,
) -> Result<Self, Error>
where
I: IntoIterator<Item = N>,
N: ToString,
{
let myviews: Vec<String> = views.into_iter().map(|ts| N::to_string(&ts)).collect();
let cfs = DB::list_cf(&opts, db_path).unwrap_or_else(|_| vec![]);
let db = if cfs.is_empty() {
DB::open(&opts, db_path)?
} else {
DB::open_cf(&opts, db_path, cfs)?
};
Ok(Self {
db,
db_path: db_path.to_path_buf(),
views: myviews,
mapper,
key_sep: vec![0],
})
}
pub fn separator(mut self, sep: &[u8]) -> Self {
if sep.is_empty() {
panic!("separator must be one or more bytes");
}
self.key_sep = sep.to_owned();
self
}
pub fn db(&self) -> &DB {
&self.db
}
pub fn mut_db(&mut self) -> &mut DB {
&mut self.db
}
pub fn put<D, K>(&mut self, key: K, value: &D) -> Result<(), Error>
where
D: Document,
K: AsRef<[u8]>,
{
let bytes = value.to_bytes()?;
self.db.put(key.as_ref(), bytes)?;
self.update_changes(key.as_ref())?;
for view in &self.views {
let mut mrview = String::from(VIEW_PREFIX);
mrview.push_str(&view);
if let Some(cf) = self.db.cf_handle(&mrview) {
let emitter = Emitter::new(&self.db, key.as_ref(), &cf, &self.key_sep);
D::map(&value, &view, &emitter)?;
}
}
Ok(())
}
pub fn get<D, K>(&self, key: K) -> Result<Option<D>, Error>
where
D: Document,
K: AsRef<[u8]>,
{
let result = self.db.get(key.as_ref())?;
match result {
Some(v) => Ok(Some(D::from_bytes(key.as_ref(), &v)?)),
None => Ok(None),
}
}
pub fn delete<K: AsRef<[u8]>>(&mut self, key: K) -> Result<(), Error> {
self.db.delete(key.as_ref())?;
self.update_changes(key.as_ref())?;
Ok(())
}
fn update_changes<K>(&mut self, key: K) -> Result<(), Error>
where
K: AsRef<[u8]>,
{
let cf = match self.db.cf_handle(CHANGES_CF) {
None => {
let opts = Options::default();
self.db.create_cf(CHANGES_CF, &opts)?;
self.db
.cf_handle(CHANGES_CF)
.ok_or_else(|| err_msg("missing changes column family"))?
}
Some(cf) => cf,
};
let seq = self.db.latest_sequence_number();
self.db.put_cf(cf, key.as_ref(), &seq.to_le_bytes())?;
Ok(())
}
pub fn query(&mut self, view: &str) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self.db.iterator_cf(&cf, IteratorMode::Start);
Ok(QueryIterator::new(&self.db, iter, &self.key_sep, &cf))
}
pub fn query_by_key<K: AsRef<[u8]>>(
&mut self,
view: &str,
key: K,
) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self.db.prefix_iterator_cf(&cf, key.as_ref());
Ok(QueryIterator::new_prefix(
&self.db,
iter,
&self.key_sep,
cf,
key.as_ref(),
))
}
pub fn query_all_keys<I, N>(&mut self, view: &str, keys: I) -> Result<Vec<QueryResult>, Error>
where
I: IntoIterator<Item = N>,
N: AsRef<[u8]>,
{
let mut query_results: Vec<QueryResult> = Vec::new();
let mut key_count: usize = 0;
#[allow(clippy::explicit_counter_loop)]
for key in keys.into_iter() {
for item in self.query_by_key(view, N::as_ref(&key))? {
query_results.push(item);
}
key_count += 1;
}
let mut key_counts: HashMap<Box<[u8]>, usize> = HashMap::new();
query_results.iter().for_each(|r| {
if let Some(value) = key_counts.get_mut(r.doc_id.as_ref()) {
*value += 1;
} else {
key_counts.insert(r.doc_id.clone(), 1);
}
});
let mut matching_rows: Vec<QueryResult> = query_results
.drain(..)
.filter(|r| key_counts[r.doc_id.as_ref()] == key_count)
.collect();
matching_rows.sort_unstable_by(|a, b| a.doc_id.as_ref().cmp(b.doc_id.as_ref()));
matching_rows.dedup_by(|a, b| a.doc_id.as_ref() == b.doc_id.as_ref());
Ok(matching_rows)
}
pub fn query_exact<K: AsRef<[u8]>>(
&mut self,
view: &str,
key: K,
) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let len = key.as_ref().len() + self.key_sep.len();
let mut prefix: Vec<u8> = Vec::with_capacity(len);
prefix.extend_from_slice(&key.as_ref()[..]);
prefix.extend_from_slice(&self.key_sep);
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self.db.prefix_iterator_cf(&cf, &prefix);
Ok(QueryIterator::new_prefix(
&self.db,
iter,
&self.key_sep,
cf,
&prefix,
))
}
pub fn query_range<K: AsRef<[u8]>>(
&mut self,
view: &str,
key_a: K,
key_b: K,
) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self
.db
.iterator_cf(&cf, IteratorMode::From(key_a.as_ref(), Direction::Forward));
Ok(QueryIterator::new_range(
&self.db,
iter,
&self.key_sep,
cf,
key_b.as_ref(),
))
}
pub fn query_greater_than<K: AsRef<[u8]>>(
&mut self,
view: &str,
key: K,
) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self
.db
.iterator_cf(&cf, IteratorMode::From(key.as_ref(), Direction::Forward));
Ok(QueryIterator::new(&self.db, iter, &self.key_sep, cf))
}
pub fn query_less_than<K: AsRef<[u8]>>(
&mut self,
view: &str,
key: K,
) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self.db.iterator_cf(&cf, IteratorMode::Start);
Ok(QueryIterator::new_range(
&self.db,
iter,
&self.key_sep,
cf,
key.as_ref(),
))
}
pub fn query_desc<K: AsRef<[u8]>>(
&mut self,
view: &str,
key: K,
) -> Result<QueryIterator, Error> {
let mrview = self.ensure_view_built(view)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self
.db
.iterator_cf(&cf, IteratorMode::From(key.as_ref(), Direction::Reverse));
Ok(QueryIterator::new(&self.db, iter, &self.key_sep, cf))
}
pub fn count_by_key<K: AsRef<[u8]>>(&mut self, view: &str, key: K) -> Result<usize, Error> {
let qiter = self.query_by_key(view, key)?;
Ok(qiter.count())
}
pub fn count_all_keys(&mut self, view: &str) -> Result<HashMap<Box<[u8]>, usize>, Error> {
let iter = self.query(view)?;
let mut key_counts: HashMap<Box<[u8]>, usize> = HashMap::new();
iter.for_each(|r| {
if let Some(value) = key_counts.get_mut(r.key.as_ref()) {
*value += 1;
} else {
key_counts.insert(r.key.clone(), 1);
}
});
Ok(key_counts)
}
pub fn rebuild(&mut self, view: &str) -> Result<(), Error> {
let mut mrview = String::from(VIEW_PREFIX);
mrview.push_str(view);
if let Some(_cf) = self.db.cf_handle(&mrview) {
self.db.drop_cf(&mrview)?;
}
self.build(view, &mrview)
}
fn ensure_view_built(&mut self, view: &str) -> Result<String, Error> {
if self.views.iter().any(|v| v == view) {
let mut mrview = String::from(VIEW_PREFIX);
mrview.push_str(view);
if self.db.cf_handle(&mrview).is_none() {
self.build(view, &mrview)?;
};
Ok(mrview)
} else {
panic!("\"{:}\" is not a registered view", view);
}
}
fn build(&mut self, view: &str, mrview: &str) -> Result<(), Error> {
let opts = Options::default();
self.db.create_cf(&mrview, &opts)?;
let cf = self
.db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing view column family"))?;
let iter = self.db.iterator(IteratorMode::Start);
for (key, value) in iter {
let emitter = Emitter::new(&self.db, &key, cf, &self.key_sep);
(*self.mapper)(&key, &value, view, &emitter)?;
}
Ok(())
}
pub fn delete_index(&mut self, view: &str) -> Result<(), Error> {
let mut mrview = String::from(VIEW_PREFIX);
mrview.push_str(view);
if let Some(_cf) = self.db.cf_handle(&mrview) {
self.db.drop_cf(&mrview)?;
}
if let Some(idx) = self.views.iter().position(|v| v == view) {
self.views.swap_remove(idx);
}
Ok(())
}
pub fn index_cleanup(&mut self) -> Result<(), Error> {
let view_names: Vec<String> = self
.views
.iter()
.map(|v| {
let mut mrview = String::from(VIEW_PREFIX);
mrview.push_str(v);
mrview
})
.collect();
let opts = Options::default();
let names = DB::list_cf(&opts, &self.db_path)?;
for unknown in names
.iter()
.filter(|cf| view_names.iter().all(|v| v != *cf))
{
if unknown.starts_with(VIEW_PREFIX) {
self.db.drop_cf(unknown)?;
}
}
Ok(())
}
}
fn read_le_u64(input: &[u8]) -> u64 {
let (int_bytes, _rest) = input.split_at(std::mem::size_of::<u64>());
u64::from_le_bytes(int_bytes.try_into().unwrap())
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct QueryResult {
pub key: Box<[u8]>,
pub value: Box<[u8]>,
pub doc_id: Box<[u8]>,
}
impl QueryResult {
fn new(key: Box<[u8]>, value: Box<[u8]>, doc_id: Box<[u8]>) -> Self {
Self { key, value, doc_id }
}
}
pub struct QueryIterator<'a> {
db: &'a DB,
dbiter: DBIterator<'a>,
prefix: Option<Vec<u8>>,
upper: Option<Vec<u8>>,
key_sep: Vec<u8>,
cf: &'a ColumnFamily,
seq_len: usize,
}
impl<'a> QueryIterator<'a> {
fn new(db: &'a DB, dbiter: DBIterator<'a>, sep: &[u8], cf: &'a ColumnFamily) -> Self {
let u64_len = std::mem::size_of::<u64>();
Self {
db,
dbiter,
prefix: None,
upper: None,
key_sep: sep.to_owned(),
cf,
seq_len: u64_len,
}
}
fn new_prefix(
db: &'a DB,
dbiter: DBIterator<'a>,
sep: &[u8],
cf: &'a ColumnFamily,
prefix: &[u8],
) -> Self {
let u64_len = std::mem::size_of::<u64>();
Self {
db,
dbiter,
prefix: Some(prefix.to_owned()),
upper: None,
key_sep: sep.to_owned(),
cf,
seq_len: u64_len,
}
}
fn new_range(
db: &'a DB,
dbiter: DBIterator<'a>,
sep: &[u8],
cf: &'a ColumnFamily,
upper: &[u8],
) -> Self {
let u64_len = std::mem::size_of::<u64>();
Self {
db,
dbiter,
prefix: None,
upper: Some(upper.to_owned()),
key_sep: sep.to_owned(),
cf,
seq_len: u64_len,
}
}
fn find_separator(&self, key: &[u8]) -> usize {
if self.key_sep.len() == 1 {
if let Some(pos) = key.iter().position(|&x| x == self.key_sep[0]) {
return pos;
}
return 0;
}
for (idx, win) in key.windows(self.key_sep.len()).enumerate() {
if win == &self.key_sep[..] {
return idx;
}
}
0
}
fn is_stale(&self, key: &[u8], seq: &[u8]) -> bool {
if let Some(cf) = self.db.cf_handle(CHANGES_CF) {
if let Ok(Some(val)) = self.db.get_cf(cf, key) {
let index_seq = read_le_u64(seq);
let changed_seq = read_le_u64(&val);
return index_seq < changed_seq;
}
}
false
}
}
impl<'a> Iterator for QueryIterator<'a> {
type Item = QueryResult;
fn next(&mut self) -> Option<Self::Item> {
while let Some((key, value)) = self.dbiter.next() {
if let Some(prefix) = &self.prefix {
if key.len() < prefix.len() {
return None;
}
if key[..prefix.len()] != prefix[..] {
return None;
}
}
let sep_pos = self.find_separator(&key);
let short_key = key[..sep_pos].to_vec();
if let Some(upper) = &self.upper {
let overlap = if short_key.len() < upper.len() {
short_key.len()
} else {
upper.len()
};
if short_key[..overlap] >= upper[..overlap] {
return None;
}
}
let doc_id = key[sep_pos + self.key_sep.len()..].to_vec();
let seq = value[..self.seq_len].to_vec();
if self.is_stale(&doc_id, &seq) {
let _ = self.db.delete_cf(&self.cf, key);
} else {
let ivalue = value[self.seq_len..].to_vec();
return Some(QueryResult::new(
short_key.into_boxed_slice(),
ivalue.into_boxed_slice(),
doc_id.into_boxed_slice(),
));
}
}
None
}
}
impl<'a> fmt::Debug for QueryIterator<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "QueryIterator")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::str;
#[derive(Serialize, Deserialize)]
struct LenVal {
#[serde(skip)]
key: String,
len: usize,
val: String,
}
impl Document for LenVal {
fn from_bytes(key: &[u8], value: &[u8]) -> Result<Self, Error> {
let mut serde_result: LenVal = serde_cbor::from_slice(value)?;
serde_result.key = str::from_utf8(key)?.to_owned();
Ok(serde_result)
}
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let encoded: Vec<u8> = serde_cbor::to_vec(self)?;
Ok(encoded)
}
fn map(&self, view: &str, emitter: &Emitter) -> Result<(), Error> {
if view == "value" {
emitter.emit(self.val.as_bytes(), None)?;
}
Ok(())
}
}
#[test]
fn open_drop_open() {
let db_path = "tmp/test/open_drop_open";
let _ = fs::remove_dir_all(db_path);
let views = vec!["value".to_owned()];
let document = LenVal {
key: String::from("lv/deadbeef"),
len: 12,
val: String::from("deceased cow"),
};
let key = document.key.as_bytes();
{
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
let result = dbase.query("value");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0].key.as_ref(), b"deceased cow");
assert_eq!(results[0].doc_id.as_ref(), b"lv/deadbeef");
assert_eq!(results[0].value.as_ref(), b"");
}
let mut opts = Options::default();
opts.create_if_missing(true);
let mut dbase = Database::open(Path::new(db_path), &views, Box::new(mapper), opts).unwrap();
let result: Result<Option<LenVal>, Error> = dbase.get(&key);
assert!(result.is_ok());
let option = result.unwrap();
assert!(option.is_some());
let actual = option.unwrap();
assert_eq!(document.key, actual.key);
assert_eq!(document.len, actual.len);
assert_eq!(document.val, actual.val);
let result = dbase.delete(&key);
assert!(result.is_ok());
let result: Result<Option<LenVal>, Error> = dbase.get(&key);
assert!(result.is_ok());
let option = result.unwrap();
assert!(option.is_none());
let result = dbase.delete(&key);
assert!(result.is_ok());
}
#[test]
fn open_existing_empty() {
let db_path = "tmp/test/open_existing_empty";
let _ = fs::remove_dir_all(db_path);
let _ = fs::create_dir_all(db_path);
let views = vec!["value".to_owned()];
let path: &Path = Path::new(db_path);
let result = Database::open_default(path, &views, Box::new(mapper));
assert!(result.is_ok(), "opened database successfully");
}
#[test]
fn open_threaded() {
let db_path = "tmp/test/open_threaded";
let _ = fs::remove_dir_all(db_path);
let views = vec!["value".to_owned()];
use std::sync::{Arc, Mutex};
let refs: Mutex<HashMap<PathBuf, Arc<Database>>> = Mutex::new(HashMap::new());
let path: &Path = Path::new(db_path);
{
let mut db_refs = refs.lock().unwrap();
let dbase = Database::open_default(path, &views, Box::new(mapper)).unwrap();
let arc = Arc::new(dbase);
let buf: PathBuf = path.to_path_buf();
db_refs.insert(buf, arc);
}
std::thread::spawn(move || {
let db_refs = refs.lock().unwrap();
if let Some(dbase) = db_refs.get(path) {
let result: Result<Option<LenVal>, Error> = dbase.get(b"foobar");
assert!(result.is_ok());
}
});
}
#[derive(Serialize, Deserialize)]
struct Asset {
#[serde(skip)]
key: String,
location: String,
tags: Vec<String>,
}
impl Document for Asset {
fn from_bytes(key: &[u8], value: &[u8]) -> Result<Self, Error> {
let mut serde_result: Asset = serde_cbor::from_slice(value)?;
serde_result.key = str::from_utf8(key)?.to_owned();
Ok(serde_result)
}
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let encoded: Vec<u8> = serde_cbor::to_vec(self)?;
Ok(encoded)
}
fn map(&self, view: &str, emitter: &Emitter) -> Result<(), Error> {
if view == "tags" {
for tag in &self.tags {
emitter.emit(tag.as_bytes(), Some(&self.location.as_bytes()))?;
}
} else if view == "location" {
emitter.emit(self.location.as_bytes(), None)?;
}
Ok(())
}
}
fn mapper(key: &[u8], value: &[u8], view: &str, emitter: &Emitter) -> Result<(), Error> {
if &key[..3] == b"lv/".as_ref() {
let doc = LenVal::from_bytes(key, value)?;
doc.map(view, emitter)?;
} else if &key[..3] == b"as/".as_ref() {
let doc = Asset::from_bytes(key, value)?;
doc.map(view, emitter)?;
}
Ok(())
}
#[test]
fn asset_view_creation() {
let db_path = "tmp/test/asset_view_creation";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let document = Asset {
key: String::from("as/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
let result = dbase.query("tags");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.doc_id.as_ref() == b"as/cafebabe"));
assert!(results.iter().all(|r| r.value.as_ref() == b"hawaii"));
let tags: Vec<String> = results
.iter()
.map(|r| str::from_utf8(&r.key).unwrap().to_owned())
.collect();
assert_eq!(tags.len(), 3);
assert!(tags.contains(&String::from("cat")));
assert!(tags.contains(&String::from("black")));
assert!(tags.contains(&String::from("tail")));
}
#[test]
fn no_view_creation() {
let db_path = "tmp/test/no_view_creation";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let document = Asset {
key: String::from("none/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
assert_eq!(count_index(&mut dbase, "tags"), 0);
}
#[test]
fn empty_view_name() {
let db_path = "tmp/test/empty_view_name";
let _ = fs::remove_dir_all(db_path);
let views = vec!["".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let document = Asset {
key: String::from("as/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
assert_eq!(count_index(&mut dbase, ""), 0);
}
#[test]
fn empty_view_list() {
let db_path = "tmp/test/empty_view_list";
let _ = fs::remove_dir_all(db_path);
let views: Vec<String> = Vec::new();
let mut dbase =
Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let document = Asset {
key: String::from("as/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
}
#[test]
#[should_panic]
fn query_on_unknown_view() {
let db_path = "tmp/test/query_on_unknown_view";
let _ = fs::remove_dir_all(db_path);
let views = vec!["viewname".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let _ = dbase.query("nonesuch");
}
#[test]
fn query_by_key() {
let db_path = "tmp/test/query_by_key";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/cafed00d"),
location: String::from("taiwan"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/xxxyyyzz"),
location: String::from("london"),
tags: vec![
String::from("cat"),
String::from("orange"),
String::from("fur"),
],
},
Asset {
key: String::from("as/mmmnnnoo"),
location: String::from("new york"),
tags: vec![
String::from("cat"),
String::from("orange"),
String::from("fur"),
],
},
Asset {
key: String::from("as/jjjkkkll"),
location: String::from("san diego"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("fur"),
],
},
Asset {
key: String::from("as/1badb002"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("ears"),
],
},
Asset {
key: String::from("as/baadf00d"),
location: String::from("dublin"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
assert_eq!(count_index(&mut dbase, "tags"), 21);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"nonesuch"), 0);
let result = dbase.query_by_key("tags", b"cat");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 5);
assert!(results.iter().all(|r| r.key.as_ref() == b"cat"));
let keys: Vec<String> = results
.iter()
.map(|r| str::from_utf8(&r.doc_id).unwrap().to_owned())
.collect();
assert_eq!(keys.len(), 5);
assert_eq!(keys[0], "as/1badb002");
assert_eq!(keys[1], "as/cafebabe");
assert_eq!(keys[2], "as/jjjkkkll");
assert_eq!(keys[3], "as/mmmnnnoo");
assert_eq!(keys[4], "as/xxxyyyzz");
let result = dbase.query_desc("tags", b"cat");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 3);
assert!(results.iter().all(|r| r.key.as_ref() == b"black"));
let keys: Vec<String> = results
.iter()
.map(|r| str::from_utf8(&r.doc_id).unwrap().to_owned())
.collect();
assert_eq!(keys.len(), 3);
assert_eq!(keys[0], "as/cafed00d");
assert_eq!(keys[1], "as/cafebabe");
assert_eq!(keys[2], "as/1badb002");
}
#[test]
fn query_by_key_separator() {
let db_path = "tmp/test/query_by_key_separator";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let dbase = Database::open_default(Path::new(db_path), views, Box::new(mapper)).unwrap();
let sep = vec![0xff, 0xff, 0xff];
let mut dbase = dbase.separator(&sep);
let documents = [
Asset {
key: String::from("as/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/cafed00d"),
location: String::from("taiwan"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/1badb002"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("ears"),
],
},
Asset {
key: String::from("as/baadf00d"),
location: String::from("dublin"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
let result = dbase.query_by_key("tags", b"cat");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| r.key.as_ref() == b"cat"));
let keys: Vec<String> = results
.iter()
.map(|r| str::from_utf8(&r.doc_id).unwrap().to_owned())
.collect();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&String::from("as/cafebabe")));
assert!(keys.contains(&String::from("as/1badb002")));
}
fn count_index(dbase: &mut Database, view: &str) -> usize {
let result = dbase.query(view);
assert!(result.is_ok());
let iter = result.unwrap();
iter.count()
}
fn count_index_by_query(dbase: &mut Database, view: &str, query: &[u8]) -> usize {
let result = dbase.query_by_key(view, query);
assert!(result.is_ok());
let iter = result.unwrap();
iter.count()
}
fn count_index_exact(dbase: &mut Database, view: &str, query: &[u8]) -> usize {
let result = dbase.query_exact(view, query);
assert!(result.is_ok());
let iter = result.unwrap();
iter.count()
}
fn count_index_records(db: &DB, view: &str) -> usize {
let mut mrview = String::from(VIEW_PREFIX);
mrview.push_str(view);
let result = db
.cf_handle(&mrview)
.ok_or_else(|| err_msg("missing column family"));
assert!(result.is_ok());
let cf = result.unwrap();
let iter = db.iterator_cf(cf, IteratorMode::Start);
iter.count()
}
#[test]
fn query_with_changes() {
let db_path = "tmp/test/query_with_changes";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/cafebabe"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/cafed00d"),
location: String::from("taiwan"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/1badb002"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("ears"),
],
},
Asset {
key: String::from("as/baadf00d"),
location: String::from("dublin"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
assert_eq!(count_index_by_query(&mut dbase, "tags", b"fur"), 1);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"tail"), 2);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"cat"), 2);
assert_eq!(count_index_records(dbase.db(), "tags"), 12);
let documents = [
Asset {
key: String::from("as/cafed00d"),
location: String::from("taiwan"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fuzz"),
],
},
Asset {
key: String::from("as/1badb002"),
location: String::from("hakone"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
assert_eq!(count_index_records(dbase.db(), "tags"), 16);
assert_eq!(count_index(&mut dbase, "tags"), 12);
assert_eq!(count_index_records(dbase.db(), "tags"), 12);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"cat"), 1);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"fur"), 0);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"fuzz"), 1);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"tail"), 3);
let result = dbase.delete(String::from("as/baadf00d").as_bytes());
assert!(result.is_ok());
assert_eq!(count_index(&mut dbase, "tags"), 9);
assert_eq!(count_index_records(dbase.db(), "tags"), 9);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"mouse"), 0);
}
#[test]
fn query_exact() {
let db_path = "tmp/test/query_exact";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/onecat"),
location: String::from("tree"),
tags: vec![
String::from("cat"),
String::from("orange"),
String::from("tail"),
],
},
Asset {
key: String::from("as/twocats"),
location: String::from("backyard"),
tags: vec![
String::from("cats"),
String::from("oranges"),
String::from("tails"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
assert_eq!(count_index_by_query(&mut dbase, "tags", b"cat"), 2);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"cats"), 1);
assert_eq!(count_index_by_query(&mut dbase, "tags", b"tail"), 2);
assert_eq!(count_index_exact(&mut dbase, "tags", b"cat"), 1);
assert_eq!(count_index_exact(&mut dbase, "tags", b"orange"), 1);
assert_eq!(count_index_exact(&mut dbase, "tags", b"tail"), 1);
}
#[allow(clippy::cognitive_complexity)]
#[test]
fn query_range_text() {
let db_path = "tmp/test/query_range_text";
let _ = fs::remove_dir_all(db_path);
let fruits = [
"apple",
"avocado",
"banana",
"cantaloupe",
"durian",
"grape",
"lemon",
"mandarin",
"mango",
"orange",
"peach",
"pear",
"strawberry",
"tangerine",
"watermelon",
];
let views = vec!["value".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
for (idx, fruit_name) in fruits.iter().enumerate() {
let key = format!("lv/fruit{}", idx);
let document = LenVal {
key,
len: fruit_name.len(),
val: String::from(*fruit_name),
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
}
let result = dbase.query_range("value", "grape", "pear");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 6);
assert_eq!(results[0].key.as_ref(), b"grape");
assert_eq!(results[1].key.as_ref(), b"lemon");
assert_eq!(results[2].key.as_ref(), b"mandarin");
assert_eq!(results[3].key.as_ref(), b"mango");
assert_eq!(results[4].key.as_ref(), b"orange");
assert_eq!(results[5].key.as_ref(), b"peach");
let result = dbase.query_range("value", "dog", "men");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 5);
assert_eq!(results[0].key.as_ref(), b"durian");
assert_eq!(results[1].key.as_ref(), b"grape");
assert_eq!(results[2].key.as_ref(), b"lemon");
assert_eq!(results[3].key.as_ref(), b"mandarin");
assert_eq!(results[4].key.as_ref(), b"mango");
let result = dbase.query_range("value", "aaaaaaaa", "durian");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 4);
assert_eq!(results[0].key.as_ref(), b"apple");
assert_eq!(results[1].key.as_ref(), b"avocado");
assert_eq!(results[2].key.as_ref(), b"banana");
assert_eq!(results[3].key.as_ref(), b"cantaloupe");
let result = dbase.query_range("value", "pear", "xylophone");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 4);
assert_eq!(results[0].key.as_ref(), b"pear");
assert_eq!(results[1].key.as_ref(), b"strawberry");
assert_eq!(results[2].key.as_ref(), b"tangerine");
assert_eq!(results[3].key.as_ref(), b"watermelon");
let result = dbase.query_range("value", "eeeee", "fffff");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 0);
}
#[test]
fn query_greater_than() {
let db_path = "tmp/test/query_greater_than";
let _ = fs::remove_dir_all(db_path);
let fruits = [
"apple",
"avocado",
"banana",
"cantaloupe",
"durian",
"grape",
"lemon",
"mandarin",
"mango",
"orange",
"peach",
"pear",
"strawberry",
"tangerine",
"watermelon",
];
let views = vec!["value".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
for (idx, fruit_name) in fruits.iter().enumerate() {
let key = format!("lv/fruit{}", idx);
let document = LenVal {
key,
len: fruit_name.len(),
val: String::from(*fruit_name),
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
}
let result = dbase.query_greater_than("value", "mango");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 7);
assert_eq!(results[0].key.as_ref(), b"mango");
assert_eq!(results[1].key.as_ref(), b"orange");
assert_eq!(results[2].key.as_ref(), b"peach");
assert_eq!(results[3].key.as_ref(), b"pear");
assert_eq!(results[4].key.as_ref(), b"strawberry");
assert_eq!(results[5].key.as_ref(), b"tangerine");
assert_eq!(results[6].key.as_ref(), b"watermelon");
}
#[test]
fn query_less_than() {
let db_path = "tmp/test/query_less_than";
let _ = fs::remove_dir_all(db_path);
let fruits = [
"apple",
"avocado",
"banana",
"cantaloupe",
"durian",
"grape",
"lemon",
"mandarin",
"mango",
"orange",
"peach",
"pear",
"strawberry",
"tangerine",
"watermelon",
];
let views = vec!["value".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
for (idx, fruit_name) in fruits.iter().enumerate() {
let key = format!("lv/fruit{}", idx);
let document = LenVal {
key,
len: fruit_name.len(),
val: String::from(*fruit_name),
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
}
let result = dbase.query_less_than("value", "mandarin");
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 7);
assert_eq!(results[0].key.as_ref(), b"apple");
assert_eq!(results[1].key.as_ref(), b"avocado");
assert_eq!(results[2].key.as_ref(), b"banana");
assert_eq!(results[3].key.as_ref(), b"cantaloupe");
assert_eq!(results[4].key.as_ref(), b"durian");
assert_eq!(results[5].key.as_ref(), b"grape");
assert_eq!(results[6].key.as_ref(), b"lemon");
}
#[test]
fn query_range_number() {
let db_path = "tmp/test/query_range_number";
let _ = fs::remove_dir_all(db_path);
let views = vec!["value".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
for idx in 1..100 {
let num: u64 = idx * 100;
let bytes = num.to_be_bytes().to_vec();
let encoded = base32::encode(&bytes);
let key = format!("lv/number{}", idx);
let document = LenVal {
key,
len: 8,
val: str::from_utf8(&encoded[..]).unwrap().to_owned(),
};
let key = document.key.as_bytes();
let result = dbase.put(&key, &document);
assert!(result.is_ok());
}
let raw: Vec<u8> = 500u64.to_be_bytes().to_vec();
let enca = base32::encode(&raw);
let raw: Vec<u8> = 1500u64.to_be_bytes().to_vec();
let encb = base32::encode(&raw);
let result = dbase.query_range("value", &enca, &encb);
assert!(result.is_ok());
let iter = result.unwrap();
let results: Vec<QueryResult> = iter.collect();
assert_eq!(results.len(), 10);
assert_eq!(results[0].key.as_ref(), b"00000000000V8===");
assert_eq!(results[1].key.as_ref(), b"000000000015G===");
assert_eq!(results[2].key.as_ref(), b"00000000001BO===");
assert_eq!(results[3].key.as_ref(), b"00000000001I0===");
assert_eq!(results[4].key.as_ref(), b"00000000001O8===");
assert_eq!(results[5].key.as_ref(), b"00000000001UG===");
assert_eq!(results[6].key.as_ref(), b"000000000024O===");
assert_eq!(results[7].key.as_ref(), b"00000000002B0===");
assert_eq!(results[8].key.as_ref(), b"00000000002H8===");
assert_eq!(results[9].key.as_ref(), b"00000000002NG===");
}
#[test]
fn index_rebuild_delete() {
let db_path = "tmp/test/index_rebuild_delete";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned(), "location".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/blackcat"),
location: String::from("hallows"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/blackdog"),
location: String::from("moors"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/whitecat"),
location: String::from("upstairs"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("ears"),
],
},
Asset {
key: String::from("as/whitemouse"),
location: String::from("attic"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
assert_eq!(count_index_by_query(&mut dbase, "tags", b"fur"), 1);
let result = dbase.rebuild("tags");
assert!(result.is_ok());
assert_eq!(count_index_by_query(&mut dbase, "tags", b"fur"), 1);
let result = dbase.rebuild("location");
assert!(result.is_ok());
assert_eq!(count_index_by_query(&mut dbase, "location", b"attic"), 1);
let result = dbase.delete_index("tags");
assert!(result.is_ok());
let result = dbase.delete_index("location");
assert!(result.is_ok());
let result = dbase.delete_index("nonesuch");
assert!(result.is_ok());
}
#[test]
fn index_cleanup() {
let db_path = "tmp/test/index_cleanup";
let _ = fs::remove_dir_all(db_path);
let myview = "mycolumnfamily";
{
let views = vec!["tags".to_owned(), "location".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/blackcat"),
location: String::from("hallows"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/blackdog"),
location: String::from("moors"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/whitecat"),
location: String::from("upstairs"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("ears"),
],
},
Asset {
key: String::from("as/whitemouse"),
location: String::from("attic"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
assert_eq!(count_index_by_query(&mut dbase, "tags", b"fur"), 1);
assert_eq!(count_index_by_query(&mut dbase, "location", b"attic"), 1);
let opts = Options::default();
let db = dbase.mut_db();
let result = db.create_cf(myview, &opts);
assert!(result.is_ok());
let cf = db.cf_handle(myview).unwrap();
let result = db.put_cf(cf, b"mykey", b"myvalue");
assert!(result.is_ok());
}
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let result = dbase.index_cleanup();
assert!(result.is_ok());
let db = dbase.db();
let opt = db.cf_handle("mrview-tags");
assert!(opt.is_some());
let opt = db.cf_handle("mrview-location");
assert!(opt.is_none());
let opt = db.cf_handle(myview);
assert!(opt.is_some());
}
#[test]
fn query_all_keys() {
let db_path = "tmp/test/query_all_keys";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/blackcat"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/blackdog"),
location: String::from("taiwan"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/whitecatears"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("ears"),
],
},
Asset {
key: String::from("as/whitecattail"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("tail"),
],
},
Asset {
key: String::from("as/whitemouse"),
location: String::from("dublin"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
let keys: Vec<&[u8]> = vec![b"cat", b"white"];
let result = dbase.query_all_keys("tags", &keys);
assert!(result.is_ok());
let results: Vec<QueryResult> = result.unwrap().drain(..).collect();
assert_eq!(results.len(), 2);
let keys: Vec<String> = results
.iter()
.map(|r| str::from_utf8(&r.doc_id).unwrap().to_owned())
.collect();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&String::from("as/whitecatears")));
assert!(keys.contains(&String::from("as/whitecattail")));
}
#[test]
fn counting_keys() {
let db_path = "tmp/test/counting_keys";
let _ = fs::remove_dir_all(db_path);
let views = vec!["tags".to_owned()];
let mut dbase =
Database::open_default(Path::new(db_path), &views, Box::new(mapper)).unwrap();
let documents = [
Asset {
key: String::from("as/blackcat"),
location: String::from("hawaii"),
tags: vec![
String::from("cat"),
String::from("black"),
String::from("tail"),
],
},
Asset {
key: String::from("as/blackdog"),
location: String::from("taiwan"),
tags: vec![
String::from("dog"),
String::from("black"),
String::from("fur"),
],
},
Asset {
key: String::from("as/whitecatears"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("ears"),
],
},
Asset {
key: String::from("as/whitecattail"),
location: String::from("hakone"),
tags: vec![
String::from("cat"),
String::from("white"),
String::from("tail"),
],
},
Asset {
key: String::from("as/whitemouse"),
location: String::from("dublin"),
tags: vec![
String::from("mouse"),
String::from("white"),
String::from("tail"),
],
},
];
for document in documents.iter() {
let key = document.key.as_bytes();
let result = dbase.put(&key, document);
assert!(result.is_ok());
}
let result = dbase.count_by_key("tags", b"white");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 3);
let result = dbase.count_by_key("tags", b"black");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2);
let result = dbase.count_all_keys("tags");
assert!(result.is_ok());
let counts = result.unwrap();
assert_eq!(counts[b"white".to_vec().into_boxed_slice().as_ref()], 3);
assert_eq!(counts[b"black".to_vec().into_boxed_slice().as_ref()], 2);
assert_eq!(counts[b"dog".to_vec().into_boxed_slice().as_ref()], 1);
assert_eq!(counts[b"cat".to_vec().into_boxed_slice().as_ref()], 3);
}
}