use taladb::document::Value;
use taladb::{Database, Filter};
fn arr(items: &[&str]) -> Value {
Value::Array(items.iter().map(|s| Value::Str((*s).into())).collect())
}
fn nums(items: &[i64]) -> Value {
Value::Array(items.iter().map(|n| Value::Int(*n)).collect())
}
fn seeded(indexed: bool) -> taladb::Collection {
let db = Box::leak(Box::new(Database::open_in_memory().unwrap()));
let col = db.collection("posts").unwrap();
if indexed {
col.create_index("tags").unwrap();
col.create_index("scores").unwrap();
}
col.insert(vec![
("title".into(), Value::Str("a".into())),
("tags".into(), arr(&["rust", "db"])),
("scores".into(), nums(&[1, 5, 9])),
])
.unwrap();
col.insert(vec![
("title".into(), Value::Str("b".into())),
("tags".into(), arr(&["db", "wasm"])),
("scores".into(), nums(&[2, 3])),
])
.unwrap();
col.insert(vec![
("title".into(), Value::Str("c".into())),
("tags".into(), Value::Str("rust".into())), ("scores".into(), Value::Int(7)),
])
.unwrap();
col.insert(vec![("title".into(), Value::Str("d".into()))]) .unwrap();
col
}
fn titles(docs: &[taladb::Document]) -> Vec<String> {
let mut out: Vec<String> = docs
.iter()
.map(|d| match d.get("title") {
Some(Value::Str(s)) => s.clone(),
_ => String::new(),
})
.collect();
out.sort();
out
}
fn both(f: impl Fn(&taladb::Collection) -> Vec<String>, expected: &[&str]) {
let want: Vec<String> = expected.iter().map(|s| (*s).to_string()).collect();
assert_eq!(f(&seeded(false)), want, "unindexed");
assert_eq!(f(&seeded(true)), want, "indexed");
}
#[test]
fn equality_matches_an_element() {
both(
|col| {
titles(
&col.find(Filter::Eq("tags".into(), Value::Str("rust".into())))
.unwrap(),
)
},
&["a", "c"],
);
}
#[test]
fn equality_still_matches_the_whole_array() {
both(
|col| {
titles(
&col.find(Filter::Eq("tags".into(), arr(&["rust", "db"])))
.unwrap(),
)
},
&["a"],
);
}
#[test]
fn in_matches_any_element_against_any_candidate() {
both(
|col| {
titles(
&col.find(Filter::In(
"tags".into(),
vec![Value::Str("wasm".into()), Value::Str("swift".into())],
))
.unwrap(),
)
},
&["b"],
);
}
#[test]
fn in_with_an_unindexable_whole_array_candidate_stays_correct() {
both(
|col| {
titles(
&col.find(Filter::In(
"tags".into(),
vec![arr(&["rust", "db"]), Value::Str("missing".into())],
))
.unwrap(),
)
},
&["a"],
);
}
#[test]
fn ne_excludes_a_document_any_element_of_which_matches() {
both(
|col| {
titles(
&col.find(Filter::Ne("tags".into(), Value::Str("db".into())))
.unwrap(),
)
},
&["c", "d"],
);
}
#[test]
fn range_comparisons_match_any_element() {
both(
|col| {
titles(
&col.find(Filter::Gte("scores".into(), Value::Int(7)))
.unwrap(),
)
},
&["a", "c"],
);
}
#[test]
fn a_document_is_returned_once_however_many_elements_match() {
both(
|col| {
titles(
&col.find(Filter::Gte("scores".into(), Value::Int(0)))
.unwrap(),
)
},
&["a", "b", "c"],
);
}
#[test]
fn count_agrees_with_find() {
for indexed in [false, true] {
let col = seeded(indexed);
let filter = Filter::Eq("tags".into(), Value::Str("db".into()));
assert_eq!(
col.count(filter.clone()).unwrap(),
col.find(filter).unwrap().len() as u64,
"indexed = {indexed}"
);
}
}
#[test]
fn updates_keep_the_index_in_step_with_the_document() {
let col = seeded(true);
col.update_one(
Filter::Eq("title".into(), Value::Str("b".into())),
taladb::Update::Set(vec![("tags".into(), arr(&["swift"]))]),
)
.unwrap();
assert_eq!(
titles(
&col.find(Filter::Eq("tags".into(), Value::Str("wasm".into())))
.unwrap()
),
Vec::<String>::new(),
);
assert_eq!(
titles(
&col.find(Filter::Eq("tags".into(), Value::Str("swift".into())))
.unwrap()
),
vec!["b".to_string()],
);
assert_eq!(
titles(
&col.find(Filter::Eq("tags".into(), Value::Str("db".into())))
.unwrap()
),
vec!["a".to_string()],
);
}
#[test]
fn deletes_remove_every_element_entry() {
let col = seeded(true);
col.delete_one(Filter::Eq("title".into(), Value::Str("a".into())))
.unwrap();
assert_eq!(
titles(
&col.find(Filter::Eq("tags".into(), Value::Str("rust".into())))
.unwrap()
),
vec!["c".to_string()],
"a deleted document must not linger under any of its elements"
);
}
#[test]
fn an_index_created_after_the_writes_backfills_every_element() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("posts").unwrap();
col.insert(vec![
("title".into(), Value::Str("a".into())),
("tags".into(), arr(&["rust", "db"])),
])
.unwrap();
col.create_index("tags").unwrap();
assert_eq!(
titles(
&col.find(Filter::Eq("tags".into(), Value::Str("db".into())))
.unwrap()
),
vec!["a".to_string()],
);
}
#[test]
fn nested_arrays_are_not_flattened() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("posts").unwrap();
col.insert(vec![
("title".into(), Value::Str("a".into())),
("tags".into(), Value::Array(vec![arr(&["deep"])])),
])
.unwrap();
assert!(
col.find(Filter::Eq("tags".into(), Value::Str("deep".into())))
.unwrap()
.is_empty()
);
}
#[test]
fn compound_index_expands_one_array_member() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("posts").unwrap();
col.create_compound_index(&["author", "tags"]).unwrap();
col.insert(vec![
("title".into(), Value::Str("a".into())),
("author".into(), Value::Str("ana".into())),
("tags".into(), arr(&["rust", "db"])),
])
.unwrap();
let hits = col
.find(Filter::And(vec![
Filter::Eq("author".into(), Value::Str("ana".into())),
Filter::Eq("tags".into(), Value::Str("db".into())),
]))
.unwrap();
assert_eq!(
titles(&hits),
vec!["a".to_string()],
"a compound plan must reach an array member, or adding the index would \
silently shrink the result set"
);
}
#[test]
fn a_compound_index_refuses_two_array_members() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("posts").unwrap();
col.create_compound_index(&["tags", "scores"]).unwrap();
let err = col
.insert(vec![
("tags".into(), arr(&["rust", "db"])),
("scores".into(), nums(&[1, 2])),
])
.unwrap_err();
assert!(
matches!(err, taladb::TalaDbError::CompoundIndexMultipleArrays),
"the key count would be the product of the two lists, got {err:?}"
);
}