use super::*;
use crate::catalog::ValType;
use crate::value::ValueTest;
use crate::segment::Segment;
use crate::value::IndexValue;
fn i(v: i64) -> IndexValue {
IndexValue::I64(v)
}
fn seeded() -> Segment {
let mut s = Segment::with_values(1);
s.apply_with_values(b"u1", Some(i(10)), &[Some(b"tokyo")]);
s.apply_with_values(b"u2", Some(i(20)), &[Some(b"osaka")]);
s.apply_with_values(b"u3", Some(i(30)), &[None]);
s.apply_with_values(b"u4", Some(i(40)), &[Some(b"tokyo")]);
s.apply_with_values(b"u5", Some(i(50)), &[Some(b"kyoto")]);
s
}
fn clauses<'a>() -> ScalarClauses<'a> {
ScalarClauses { filters: &[], sort: None, distinct: None, facets: &[], fetch: 100 }
}
fn keys(hits: &[ScalarHit]) -> Vec<&[u8]> {
hits.iter().map(|h| h.key.as_slice()).collect()
}
#[test]
fn values_store_update_delete_follow_the_entry() {
let mut s = Segment::with_values(1);
s.apply_with_values(b"u1", Some(i(1)), &[Some(b"a")]);
assert_eq!(s.stored(b"u1", 0), Some(&b"a"[..]));
s.apply_with_values(b"u1", Some(i(2)), &[Some(b"b")]);
assert_eq!(s.stored(b"u1", 0), Some(&b"b"[..]));
s.apply_with_values(b"u1", None, &[Some(b"c")]);
assert_eq!(s.stored(b"u1", 0), None);
s.apply_with_values(b"u1", Some(i(3)), &[Some(b"d")]);
s.remove(b"u1");
assert_eq!(s.stored(b"u1", 0), None);
let plain = Segment::new();
assert_eq!(plain.stored(b"u1", 0), None);
}
#[test]
fn values_heap_joins_the_memory_term_only_when_declared() {
let mut with = Segment::with_values(1);
with.apply_with_values(b"u1", Some(i(1)), &[Some(&[b'x'; 100])]);
let mut without = Segment::new();
without.apply(b"u1", Some(i(1)));
assert!(with.stats().approx_bytes > without.stats().approx_bytes);
}
#[test]
fn filter_thins_the_driving_order_and_missing_fails() {
let s = seeded();
let t = ValueTest::eq(ValType::Str, b"tokyo").unwrap();
let filters = [(0usize, t)];
let c = ScalarClauses { filters: &filters, ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"u1"[..], b"u4"]);
assert!(page.cursor.is_none(), "page not full → exhausted");
}
#[test]
fn filter_uncoercible_stored_value_is_excluded_not_matched() {
let mut s = Segment::with_values(1);
s.apply_with_values(b"u1", Some(i(1)), &[Some(b"12")]);
s.apply_with_values(b"u2", Some(i(2)), &[Some(b"not-a-number")]);
let t = ValueTest::range(ValType::I64, b"0", b"100").unwrap();
let filters = [(0usize, t)];
let c = ScalarClauses { filters: &filters, ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"u1"[..]], "text in a numeric range is not inside it");
}
#[test]
fn filter_pages_with_a_cursor_in_driving_order() {
let s = seeded();
let t = ValueTest::range(ValType::Str, b"a", b"zz").unwrap();
let filters = [(0usize, t.clone())];
let c = ScalarClauses { filters: &filters, fetch: 2, ..clauses() };
let p1 = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&p1.hits), vec![&b"u1"[..], b"u2"]);
let cur = p1.cursor.expect("full page carries a cursor");
let p2 = s.query_claused(&i(0), &i(100), Some(&cur), &c);
assert_eq!(keys(&p2.hits), vec![&b"u4"[..], b"u5"]);
}
#[test]
fn sort_orders_by_the_stored_key_missing_last_both_directions() {
let s = seeded();
let c = ScalarClauses { sort: Some((0, false, ValType::Str)), ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"u5"[..], b"u2", b"u1", b"u4", b"u3"]);
let c = ScalarClauses { sort: Some((0, true, ValType::Str)), ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"u1"[..], b"u4", b"u2", b"u5", b"u3"]);
}
#[test]
fn sort_key_is_numeric_under_a_numeric_declaration() {
let mut s = Segment::with_values(1);
s.apply_with_values(b"a", Some(i(1)), &[Some(b"9")]);
s.apply_with_values(b"b", Some(i(2)), &[Some(b"10")]);
let c = ScalarClauses { sort: Some((0, false, ValType::I64)), ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"a"[..], b"b"], "9 < 10 numerically");
let c = ScalarClauses { sort: Some((0, false, ValType::Str)), ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"b"[..], b"a"], "\"10\" < \"9\" as text");
}
#[test]
fn distinct_collapses_during_selection_and_no_value_is_its_own_group() {
let s = seeded();
let c = ScalarClauses { distinct: Some((0, ValType::Str)), ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"u1"[..], b"u2", b"u3", b"u5"]);
}
#[test]
fn distinct_identity_is_the_coerced_value() {
let mut s = Segment::with_values(1);
s.apply_with_values(b"a", Some(i(1)), &[Some(b"1")]);
s.apply_with_values(b"b", Some(i(2)), &[Some(b"1.0")]);
let c = ScalarClauses { distinct: Some((0, ValType::F64)), ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"a"[..]], "1 and 1.0 are one f64 value");
}
#[test]
fn distinct_under_sort_keeps_the_best_group_representative() {
let mut s = Segment::with_values(2);
s.apply_with_values(b"a", Some(i(1)), &[Some(b"g1"), Some(b"5")]);
s.apply_with_values(b"b", Some(i(2)), &[Some(b"g1"), Some(b"1")]);
s.apply_with_values(b"c", Some(i(3)), &[Some(b"g2"), Some(b"3")]);
let c = ScalarClauses {
sort: Some((1, false, ValType::I64)),
distinct: Some((0, ValType::Str)),
..clauses()
};
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"b"[..], b"c"]);
}
#[test]
fn facet_counts_the_whole_match_set_before_truncation() {
let s = seeded();
let facets = [(0usize, ValType::Str)];
let c = ScalarClauses { facets: &facets, fetch: 1, ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(keys(&page.hits), vec![&b"u1"[..]], "page truncated to fetch");
let labels: Vec<(&[u8], u64)> =
page.facets[0].iter().map(|(_, l, n)| (l.as_slice(), *n)).collect();
assert_eq!(labels, vec![(&b"tokyo"[..], 2), (b"kyoto", 1), (b"osaka", 1)]);
}
#[test]
fn filter_reduces_facet_counts_distinct_does_not() {
let s = seeded();
let t = ValueTest::eq(ValType::Str, b"tokyo").unwrap();
let filters = [(0usize, t)];
let facets = [(0usize, ValType::Str)];
let c = ScalarClauses { filters: &filters, facets: &facets, ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(page.facets[0], vec![(b"tokyo".to_vec(), b"tokyo".to_vec(), 2)]);
let c = ScalarClauses {
distinct: Some((0, ValType::Str)),
facets: &facets,
..clauses()
};
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(page.facets[0][0].2, 2, "DISTINCT collapses the page, not the counts");
assert_eq!(keys(&page.hits).len(), 4, "page IS collapsed");
}
#[test]
fn selection_clauses_carry_no_cursor() {
let s = seeded();
let c = ScalarClauses { sort: Some((0, false, ValType::Str)), fetch: 2, ..clauses() };
let page = s.query_claused(&i(0), &i(100), None, &c);
assert_eq!(page.hits.len(), 2);
assert!(page.cursor.is_none());
}
#[test]
fn merge_orders_collapses_offsets_and_cuts() {
let hit = |k: &[u8], v: i64, okey: Option<&[u8]>, dkey: Option<&[u8]>| {
(
ScalarHit {
key: k.to_vec(),
value: i(v),
okey: okey.map(<[u8]>::to_vec),
dkey: dkey.map(<[u8]>::to_vec),
},
(),
)
};
let keys = |hits: &[(ScalarHit, ())]| -> Vec<Vec<u8>> {
hits.iter().map(|(h, ())| h.key.clone()).collect()
};
let merged = merge_claused(
vec![hit(b"b", 2, None, None), hit(b"a", 1, None, None), hit(b"c", 1, None, None)],
None,
false,
0,
10,
);
assert_eq!(keys(&merged), [b"a".to_vec(), b"c".to_vec(), b"b".to_vec()]);
let merged = merge_claused(
vec![
hit(b"s1", 1, Some(b"m"), Some(b"g")),
hit(b"s2", 2, Some(b"a"), Some(b"g")),
hit(b"s3", 3, None, None),
hit(b"s4", 4, Some(b"z"), Some(b"h")),
],
Some(false),
true,
1,
2,
);
assert_eq!(keys(&merged), [b"s4".to_vec(), b"s3".to_vec()]);
let merged = merge_claused(vec![hit(b"a", 1, None, None)], None, false, 99, 5);
assert!(merged.is_empty());
}
#[test]
fn facet_fold_sums_by_identity_across_shards() {
let mut acc: Vec<Vec<FacetBucket>> = vec![vec![]];
fold_facets(&mut acc, vec![vec![(b"id1".to_vec(), b"1".to_vec(), 2)]]);
fold_facets(
&mut acc,
vec![vec![(b"id1".to_vec(), b"1.0".to_vec(), 3), (b"id2".to_vec(), b"7".to_vec(), 9)]],
);
sort_facets(&mut acc);
assert_eq!(
acc[0],
vec![
(b"id2".to_vec(), b"7".to_vec(), 9),
(b"id1".to_vec(), b"1".to_vec(), 5),
],
"summed by identity; the label is the first spelling seen"
);
}
#[test]
fn scalar_sorted_order_agrees_with_the_text_contract() {
use std::cmp::Ordering;
let o = scalar_sorted_order;
assert_eq!(o((Some(b"a"), b"k1"), (Some(b"b"), b"k2"), false), Ordering::Less);
assert_eq!(o((Some(b"a"), b"k1"), (Some(b"b"), b"k2"), true), Ordering::Greater);
assert_eq!(o((Some(b"z"), b"k1"), (None, b"k2"), false), Ordering::Less);
assert_eq!(o((Some(b"z"), b"k1"), (None, b"k2"), true), Ordering::Less);
assert_eq!(o((Some(b"a"), b"k1"), (Some(b"a"), b"k2"), true), Ordering::Less);
assert_eq!(o((None, b"k1"), (None, b"k2"), true), Ordering::Less);
}