use motedb::storage::col_segment::ColSegmentStore;
use motedb::types::{ColumnType, Value};
use tempfile::TempDir;
fn make_store(col_types: Vec<ColumnType>) -> (TempDir, std::sync::Arc<ColSegmentStore>) {
let dir = TempDir::new().unwrap();
let store = ColSegmentStore::create(dir.path(), "t", col_types).unwrap();
(dir, store)
}
fn row(key: u64, ts: u64, vals: Vec<Value>) -> (u64, u64, Vec<Value>) {
(key, ts, vals)
}
#[test]
fn test_top_k_desc_returns_largest() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Float]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::Float(10.0)]),
row(2, 1, vec![Value::Integer(2), Value::Float(50.0)]),
row(3, 1, vec![Value::Integer(3), Value::Float(30.0)]),
row(4, 1, vec![Value::Integer(4), Value::Float(90.0)]),
row(5, 1, vec![Value::Integer(5), Value::Float(20.0)]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices(1, 2, true);
assert_eq!(top.len(), 2, "top-2 returns 2 indices");
let segs = store.segments_snapshot();
let mut scores: Vec<f64> = top
.iter()
.filter_map(|(s, r)| {
let seg = segs.get(*s)?;
seg.sst.read_fixed_f64(1).ok().and_then(|f| f.get_f64(*r))
})
.collect();
scores.sort_by(|a, b| b.partial_cmp(a).unwrap());
assert_eq!(scores, vec![90.0, 50.0], "DESC top-2 must be 90 and 50");
}
#[test]
fn test_top_k_asc_returns_smallest() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Float]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::Float(10.0)]),
row(2, 1, vec![Value::Integer(2), Value::Float(50.0)]),
row(3, 1, vec![Value::Integer(3), Value::Float(5.0)]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices(1, 2, false);
let segs = store.segments_snapshot();
let mut scores: Vec<f64> = top
.iter()
.filter_map(|(s, r)| {
let seg = segs.get(*s)?;
seg.sst.read_fixed_f64(1).ok().and_then(|f| f.get_f64(*r))
})
.collect();
scores.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(scores, vec![5.0, 10.0], "ASC top-2 must be 5 and 10");
}
#[test]
fn test_top_k_k_exceeds_rows() {
let (_dir, store) = make_store(vec![ColumnType::Integer]);
store
.append_rows(&[row(1, 1, vec![Value::Integer(10)])])
.unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices(0, 10, true);
assert_eq!(top.len(), 1, "k>rows returns all rows");
}
#[test]
fn test_top_k_empty_store() {
let (_dir, store) = make_store(vec![ColumnType::Integer]);
store.flush_buffer().unwrap();
let top = store.top_k_row_indices(0, 5, true);
assert!(top.is_empty(), "empty store returns no indices");
}
#[test]
fn test_top_k_k_zero() {
let (_dir, store) = make_store(vec![ColumnType::Integer]);
store
.append_rows(&[row(1, 1, vec![Value::Integer(10)])])
.unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices(0, 0, true);
assert!(top.is_empty(), "k=0 returns nothing");
}
fn fetch_floats(store: &ColSegmentStore, col: usize, idxs: &[(usize, usize)]) -> Vec<f64> {
let segs = store.segments_snapshot();
idxs.iter()
.filter_map(|(s, r)| {
let seg = segs.get(*s)?;
seg.sst.read_fixed_f64(col).ok().and_then(|f| f.get_f64(*r))
})
.collect()
}
#[test]
fn test_top_k_typed_float_desc_order() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Float]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::Float(10.0)]),
row(2, 1, vec![Value::Integer(2), Value::Float(50.0)]),
row(3, 1, vec![Value::Integer(3), Value::Float(30.0)]),
row(4, 1, vec![Value::Integer(4), Value::Float(90.0)]),
row(5, 1, vec![Value::Integer(5), Value::Float(20.0)]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices_typed(1, 3, true, true);
let scores = fetch_floats(&store, 1, &top);
assert_eq!(
scores,
vec![90.0, 50.0, 30.0],
"DESC Float top-3 must be largest-first"
);
}
#[test]
fn test_top_k_typed_float_asc_order() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Float]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::Float(10.0)]),
row(2, 1, vec![Value::Integer(2), Value::Float(50.0)]),
row(3, 1, vec![Value::Integer(3), Value::Float(5.0)]),
row(4, 1, vec![Value::Integer(4), Value::Float(30.0)]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices_typed(1, 3, false, true);
let scores = fetch_floats(&store, 1, &top);
assert_eq!(
scores,
vec![5.0, 10.0, 30.0],
"ASC Float top-3 must be smallest-first"
);
}
#[test]
fn test_top_k_typed_integer_desc_order() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Integer]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::Integer(10)]),
row(2, 1, vec![Value::Integer(2), Value::Integer(50)]),
row(3, 1, vec![Value::Integer(3), Value::Integer(30)]),
row(4, 1, vec![Value::Integer(4), Value::Integer(90)]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices_typed(1, 3, true, false);
let segs = store.segments_snapshot();
let vals: Vec<i64> = top
.iter()
.filter_map(|(s, r)| {
let seg = segs.get(*s)?;
seg.sst.read_fixed_i64(1).ok().and_then(|f| f.get_i64(*r))
})
.collect();
assert_eq!(
vals,
vec![90, 50, 30],
"DESC Integer top-3 must be largest-first"
);
}
#[test]
fn test_top_k_typed_float_with_negatives_and_zero() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Float]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::Float(-5.0)]),
row(2, 1, vec![Value::Integer(2), Value::Float(0.0)]),
row(3, 1, vec![Value::Integer(3), Value::Float(-20.0)]),
row(4, 1, vec![Value::Integer(4), Value::Float(7.5)]),
row(5, 1, vec![Value::Integer(5), Value::Float(-1.25)]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let top = store.top_k_row_indices_typed(1, 5, false, true);
let scores = fetch_floats(&store, 1, &top);
assert_eq!(
scores,
vec![-20.0, -5.0, -1.25, 0.0, 7.5],
"ASC with negatives"
);
let top = store.top_k_row_indices_typed(1, 5, true, true);
let scores = fetch_floats(&store, 1, &top);
assert_eq!(
scores,
vec![7.5, 0.0, -1.25, -5.0, -20.0],
"DESC with negatives"
);
}
#[test]
fn test_distinct_text_low_cardinality() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Text]);
let rows: Vec<_> = (0..100)
.map(|i| {
let region = match i % 3 {
0 => "US",
1 => "EU",
_ => "AS",
};
row(
i + 1,
1,
vec![Value::Integer(i as i64), Value::text(region.to_string())],
)
})
.collect();
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let vals = store.distinct_text_values(1, 10000);
let mut s: Vec<String> = vals.clone();
s.sort();
assert_eq!(
s,
vec!["AS".to_string(), "EU".to_string(), "US".to_string()],
"low-cardinality column returns exactly the distinct values"
);
}
#[test]
fn test_distinct_text_respects_max_values() {
let (_dir, store) = make_store(vec![ColumnType::Text]);
let rows: Vec<_> = (0..50)
.map(|i| row(i as u64 + 1, 1, vec![Value::text(format!("v{}", i))]))
.collect();
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let vals = store.distinct_text_values(0, 5);
assert!(vals.len() <= 5, "result capped at max_values");
assert!(vals.len() >= 1, "returns at least 1");
}
#[test]
fn test_distinct_text_empty_store() {
let (_dir, store) = make_store(vec![ColumnType::Text]);
store.flush_buffer().unwrap();
let vals = store.distinct_text_values(0, 100);
assert!(vals.is_empty(), "empty store yields no distinct values");
}
#[test]
fn test_scan_text_eq_build_matches() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Text]);
let rows: Vec<_> = vec![
row(1, 1, vec![Value::Integer(1), Value::text("US".to_string())]),
row(2, 1, vec![Value::Integer(2), Value::text("EU".to_string())]),
row(3, 1, vec![Value::Integer(3), Value::text("US".to_string())]),
row(4, 1, vec![Value::Integer(4), Value::text("US".to_string())]),
];
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let result = store.scan_text_eq_build(
1,
"US",
&[0, 1],
&[ColumnType::Integer, ColumnType::Text],
100,
);
let rows = result.expect("scan returned Some");
assert_eq!(rows.len(), 3, "3 rows match 'US'");
for r in &rows {
assert_eq!(r[1], Value::text("US".to_string()), "all rows must be US");
}
}
#[test]
fn test_scan_text_eq_build_no_match() {
let (_dir, store) = make_store(vec![ColumnType::Text]);
store
.append_rows(&[row(1, 1, vec![Value::text("a".to_string())])])
.unwrap();
store.flush_buffer().unwrap();
let result = store.scan_text_eq_build(0, "zzz", &[0], &[ColumnType::Text], 100);
let rows = result.expect("scan returned Some");
assert!(rows.is_empty(), "no match returns empty vec");
}
#[test]
fn test_scan_text_eq_build_limit() {
let (_dir, store) = make_store(vec![ColumnType::Integer, ColumnType::Text]);
let rows: Vec<_> = (0..10)
.map(|i| {
row(
i as u64 + 1,
1,
vec![Value::Integer(i), Value::text("dup".to_string())],
)
})
.collect();
store.append_rows(&rows).unwrap();
store.flush_buffer().unwrap();
let result = store.scan_text_eq_build(
1,
"dup",
&[0, 1],
&[ColumnType::Integer, ColumnType::Text],
3,
);
let rows = result.expect("scan returned Some");
assert_eq!(rows.len(), 3, "limit caps result at 3");
}