use std::collections::BTreeMap;
use vibesql_types::SqlValue;
use crate::database::indexes::{
index_metadata::IndexData, value_normalization::normalize_for_comparison,
};
fn create_test_index_data(entries: Vec<(Vec<SqlValue>, Vec<usize>)>) -> IndexData {
let mut data = BTreeMap::new();
for (key, row_indices) in entries {
let normalized_key: Vec<SqlValue> = key.iter().map(normalize_for_comparison).collect();
data.insert(normalized_key, row_indices);
}
IndexData::InMemory { data }
}
#[test]
fn test_prefix_scan_single_column_match() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(30)], vec![2]),
(vec![SqlValue::Integer(2), SqlValue::Integer(10)], vec![3]),
(vec![SqlValue::Integer(2), SqlValue::Integer(20)], vec![4]),
]);
let results = index.prefix_scan(&[SqlValue::Integer(1)]);
assert_eq!(results, vec![0, 1, 2]);
}
#[test]
fn test_prefix_scan_two_column_prefix() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(5), SqlValue::Integer(100)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(5), SqlValue::Integer(200)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(5), SqlValue::Integer(300)], vec![2]),
(vec![SqlValue::Integer(1), SqlValue::Integer(6), SqlValue::Integer(100)], vec![3]),
(vec![SqlValue::Integer(2), SqlValue::Integer(5), SqlValue::Integer(100)], vec![4]),
]);
let results = index.prefix_scan(&[SqlValue::Integer(1), SqlValue::Integer(5)]);
assert_eq!(results, vec![0, 1, 2]);
}
#[test]
fn test_prefix_scan_exact_match() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1]),
]);
let results = index.prefix_scan(&[SqlValue::Integer(1), SqlValue::Integer(10)]);
assert_eq!(results, vec![0]);
}
#[test]
fn test_prefix_scan_no_match() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(2), SqlValue::Integer(20)], vec![1]),
]);
let results = index.prefix_scan(&[SqlValue::Integer(3)]);
assert!(results.is_empty());
}
#[test]
fn test_prefix_scan_single_row() {
let index =
create_test_index_data(vec![(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0])]);
let results = index.prefix_scan(&[SqlValue::Integer(1)]);
assert_eq!(results, vec![0]);
}
#[test]
fn test_prefix_scan_multiple_rows_per_key() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0, 5, 10]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1, 6]),
]);
let results = index.prefix_scan(&[SqlValue::Integer(1)]);
assert_eq!(results, vec![0, 5, 10, 1, 6]);
}
#[test]
fn test_prefix_scan_empty_prefix() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(2), SqlValue::Integer(20)], vec![1]),
]);
let results = index.prefix_scan(&[]);
assert_eq!(results.len(), 2);
assert!(results.contains(&0));
assert!(results.contains(&1));
}
#[test]
fn test_prefix_scan_prefix_longer_than_key() {
let index =
create_test_index_data(vec![(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0])]);
let results =
index.prefix_scan(&[SqlValue::Integer(1), SqlValue::Integer(10), SqlValue::Integer(100)]);
assert!(results.is_empty());
}
#[test]
fn test_prefix_scan_empty_index() {
let index = create_test_index_data(vec![]);
let results = index.prefix_scan(&[SqlValue::Integer(1)]);
assert!(results.is_empty());
}
#[test]
fn test_prefix_scan_with_string_keys() {
let index = create_test_index_data(vec![
(vec![SqlValue::Varchar(arcstr::ArcStr::from("a")), SqlValue::Integer(1)], vec![0]),
(vec![SqlValue::Varchar(arcstr::ArcStr::from("a")), SqlValue::Integer(2)], vec![1]),
(vec![SqlValue::Varchar(arcstr::ArcStr::from("b")), SqlValue::Integer(1)], vec![2]),
]);
let results = index.prefix_scan(&[SqlValue::Varchar(arcstr::ArcStr::from("a"))]);
assert_eq!(results, vec![0, 1]);
}
#[test]
fn test_prefix_scan_with_mixed_types() {
let index = create_test_index_data(vec![
(
vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("x")),
SqlValue::Boolean(true),
],
vec![0],
),
(
vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("x")),
SqlValue::Boolean(false),
],
vec![1],
),
(
vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("y")),
SqlValue::Boolean(true),
],
vec![2],
),
]);
let results =
index.prefix_scan(&[SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("x"))]);
assert_eq!(results.len(), 2);
assert!(results.contains(&0));
assert!(results.contains(&1));
}
#[test]
fn test_prefix_scan_numeric_type_normalization() {
let index = create_test_index_data(vec![
(vec![SqlValue::Double(1.0), SqlValue::Double(10.0)], vec![0]),
(vec![SqlValue::Double(1.0), SqlValue::Double(20.0)], vec![1]),
(vec![SqlValue::Double(2.0), SqlValue::Double(10.0)], vec![2]),
]);
let results = index.prefix_scan(&[SqlValue::Integer(1)]);
assert_eq!(results, vec![0, 1]);
}
#[test]
fn test_prefix_scan_batch_basic() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(100)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(101)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(2), SqlValue::Integer(100)], vec![2]),
(vec![SqlValue::Integer(1), SqlValue::Integer(2), SqlValue::Integer(101)], vec![3]),
(vec![SqlValue::Integer(1), SqlValue::Integer(3), SqlValue::Integer(100)], vec![4]),
]);
let prefixes = vec![
vec![SqlValue::Integer(1), SqlValue::Integer(1)],
vec![SqlValue::Integer(1), SqlValue::Integer(2)],
];
let results = index.prefix_scan_batch(&prefixes);
assert_eq!(results.len(), 2);
assert_eq!(results[0], (0, vec![0, 1])); assert_eq!(results[1], (1, vec![2, 3])); }
#[test]
fn test_prefix_scan_batch_some_empty() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(1)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(3)], vec![2]),
]);
let prefixes = vec![
vec![SqlValue::Integer(1), SqlValue::Integer(1)],
vec![SqlValue::Integer(1), SqlValue::Integer(2)], vec![SqlValue::Integer(1), SqlValue::Integer(3)],
];
let results = index.prefix_scan_batch(&prefixes);
assert_eq!(results.len(), 2);
assert_eq!(results[0], (0, vec![0]));
assert_eq!(results[1], (2, vec![2]));
}
#[test]
fn test_prefix_scan_batch_all_empty() {
let index =
create_test_index_data(vec![(vec![SqlValue::Integer(1), SqlValue::Integer(1)], vec![0])]);
let prefixes = vec![
vec![SqlValue::Integer(2), SqlValue::Integer(1)],
vec![SqlValue::Integer(3), SqlValue::Integer(1)],
];
let results = index.prefix_scan_batch(&prefixes);
assert!(results.is_empty());
}
#[test]
fn test_prefix_scan_batch_empty_input() {
let index =
create_test_index_data(vec![(vec![SqlValue::Integer(1), SqlValue::Integer(1)], vec![0])]);
let results = index.prefix_scan_batch(&[]);
assert!(results.is_empty());
}
#[test]
fn test_prefix_scan_batch_tpcc_like() {
let mut entries = Vec::new();
let w_id = 1;
for d_id in 1..=10 {
for o_id in 1..=(d_id * 2) {
let key =
vec![SqlValue::Integer(w_id), SqlValue::Integer(d_id), SqlValue::Integer(o_id)];
entries.push((key, vec![((d_id - 1) * 10 + o_id - 1) as usize]));
}
}
let index = create_test_index_data(entries);
let prefixes: Vec<Vec<SqlValue>> =
(1..=10).map(|d| vec![SqlValue::Integer(w_id), SqlValue::Integer(d)]).collect();
let results = index.prefix_scan_batch(&prefixes);
assert_eq!(results.len(), 10);
for (idx, rows) in &results {
let d_id = *idx as i64 + 1;
let expected_count = (d_id * 2) as usize;
assert_eq!(
rows.len(),
expected_count,
"District {} should have {} orders",
d_id,
expected_count
);
}
}
#[test]
fn test_prefix_multi_lookup_basic() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1]),
(vec![SqlValue::Integer(2), SqlValue::Integer(10)], vec![2]),
(vec![SqlValue::Integer(2), SqlValue::Integer(20)], vec![3]),
(vec![SqlValue::Integer(3), SqlValue::Integer(10)], vec![4]),
]);
let results = index.prefix_multi_lookup(&[SqlValue::Integer(1), SqlValue::Integer(2)]);
assert_eq!(results.len(), 4);
assert!(results.contains(&0));
assert!(results.contains(&1));
assert!(results.contains(&2));
assert!(results.contains(&3));
}
#[test]
fn test_prefix_multi_lookup_with_duplicates() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(2), SqlValue::Integer(20)], vec![1]),
]);
let results = index.prefix_multi_lookup(&[
SqlValue::Integer(1),
SqlValue::Integer(1),
SqlValue::Integer(2),
]);
assert_eq!(results.len(), 2);
assert!(results.contains(&0));
assert!(results.contains(&1));
}
#[test]
fn test_prefix_scan_first_basic() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(100)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(200)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(300)], vec![2]),
(vec![SqlValue::Integer(1), SqlValue::Integer(2), SqlValue::Integer(100)], vec![3]),
]);
let result = index.prefix_scan_first(&[SqlValue::Integer(1), SqlValue::Integer(1)]);
assert_eq!(result, Some(0));
}
#[test]
fn test_prefix_scan_first_no_match() {
let index = create_test_index_data(vec![(
vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(100)],
vec![0],
)]);
let result = index.prefix_scan_first(&[SqlValue::Integer(2), SqlValue::Integer(1)]);
assert_eq!(result, None);
}
#[test]
fn test_prefix_scan_first_empty_prefix() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1)], vec![0]),
(vec![SqlValue::Integer(2)], vec![1]),
]);
let result = index.prefix_scan_first(&[]);
assert_eq!(result, Some(0));
}
#[test]
fn test_prefix_scan_first_returns_minimum_third_column() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(5), SqlValue::Integer(300)], vec![3]),
(vec![SqlValue::Integer(1), SqlValue::Integer(5), SqlValue::Integer(100)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(5), SqlValue::Integer(200)], vec![2]),
]);
let result = index.prefix_scan_first(&[SqlValue::Integer(1), SqlValue::Integer(5)]);
assert_eq!(result, Some(1)); }
#[test]
fn test_prefix_scan_limit_forward_with_limit() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(100)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(101)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(102)], vec![2]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(103)], vec![3]),
(vec![SqlValue::Integer(1), SqlValue::Integer(2), SqlValue::Integer(100)], vec![4]),
]);
let results = index.prefix_scan_limit(
&[SqlValue::Integer(1), SqlValue::Integer(1)],
Some(2),
false, );
assert_eq!(results, vec![0, 1]); }
#[test]
fn test_prefix_scan_limit_reverse_with_limit() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(100)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(101)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(102)], vec![2]),
(vec![SqlValue::Integer(1), SqlValue::Integer(1), SqlValue::Integer(103)], vec![3]),
(vec![SqlValue::Integer(1), SqlValue::Integer(2), SqlValue::Integer(100)], vec![4]),
]);
let results = index.prefix_scan_limit(
&[SqlValue::Integer(1), SqlValue::Integer(1)],
Some(1),
true, );
assert_eq!(results, vec![3]); }
#[test]
fn test_prefix_scan_limit_reverse_all() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(30)], vec![2]),
]);
let results = index.prefix_scan_limit(
&[SqlValue::Integer(1)],
None,
true, );
assert_eq!(results, vec![2, 1, 0]); }
#[test]
fn test_prefix_scan_limit_no_limit_no_reverse() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1]),
(vec![SqlValue::Integer(2), SqlValue::Integer(10)], vec![2]),
]);
let results = index.prefix_scan_limit(&[SqlValue::Integer(1)], None, false);
assert_eq!(results, vec![0, 1]);
}
#[test]
fn test_prefix_scan_limit_tpcc_order_status() {
let mut entries = Vec::new();
let w_id = 1;
let d_id = 5;
let c_id = 42;
for o_id in 1..=30 {
let key = vec![
SqlValue::Integer(w_id),
SqlValue::Integer(d_id),
SqlValue::Integer(c_id),
SqlValue::Integer(o_id),
];
entries.push((key, vec![o_id as usize - 1]));
}
let index = create_test_index_data(entries);
let results = index.prefix_scan_limit(
&[SqlValue::Integer(w_id), SqlValue::Integer(d_id), SqlValue::Integer(c_id)],
Some(1),
true, );
assert_eq!(results.len(), 1);
assert_eq!(results[0], 29);
}
#[test]
fn test_prefix_scan_limit_empty_result() {
let index =
create_test_index_data(vec![(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0])]);
let results = index.prefix_scan_limit(&[SqlValue::Integer(2)], Some(5), true);
assert!(results.is_empty());
}
#[test]
fn test_prefix_scan_limit_less_than_limit() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![1]),
]);
let results = index.prefix_scan_limit(&[SqlValue::Integer(1)], Some(10), false);
assert_eq!(results, vec![0, 1]); }
#[test]
fn test_get_distinct_first_column_values() {
let index = create_test_index_data(vec![
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("East")), SqlValue::Integer(20240101)],
vec![0],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("East")), SqlValue::Integer(20240102)],
vec![1],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("West")), SqlValue::Integer(20240101)],
vec![2],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("West")), SqlValue::Integer(20240103)],
vec![3],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("North")), SqlValue::Integer(20240102)],
vec![4],
),
]);
let distinct = index.get_distinct_first_column_values();
assert_eq!(distinct.len(), 3);
}
#[test]
fn test_skip_scan_equality_basic() {
let index = create_test_index_data(vec![
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("East")), SqlValue::Integer(20240101)],
vec![0],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("East")), SqlValue::Integer(20240102)],
vec![1],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("West")), SqlValue::Integer(20240101)],
vec![2],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("West")), SqlValue::Integer(20240103)],
vec![3],
),
]);
let results = index.skip_scan_equality(1, &SqlValue::Integer(20240101));
assert_eq!(results.len(), 2);
assert!(results.contains(&0));
assert!(results.contains(&2));
}
#[test]
fn test_skip_scan_equality_no_match() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(2), SqlValue::Integer(20)], vec![1]),
]);
let results = index.skip_scan_equality(1, &SqlValue::Integer(99));
assert!(results.is_empty());
}
#[test]
fn test_skip_scan_range_basic() {
let index = create_test_index_data(vec![
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("Electronics")), SqlValue::Integer(5)],
vec![0],
),
(
vec![SqlValue::Varchar(arcstr::ArcStr::from("Electronics")), SqlValue::Integer(15)],
vec![1],
),
(vec![SqlValue::Varchar(arcstr::ArcStr::from("Books")), SqlValue::Integer(8)], vec![2]),
(vec![SqlValue::Varchar(arcstr::ArcStr::from("Books")), SqlValue::Integer(12)], vec![3]),
(vec![SqlValue::Varchar(arcstr::ArcStr::from("Books")), SqlValue::Integer(25)], vec![4]),
]);
let results = index.skip_scan_range(
1,
Some(&SqlValue::Integer(10)),
true, Some(&SqlValue::Integer(20)),
true, );
assert_eq!(results.len(), 2);
assert!(results.contains(&1));
assert!(results.contains(&3));
}
#[test]
fn test_skip_scan_range_exclusive_bounds() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(1), SqlValue::Integer(10)], vec![0]),
(vec![SqlValue::Integer(1), SqlValue::Integer(15)], vec![1]),
(vec![SqlValue::Integer(1), SqlValue::Integer(20)], vec![2]),
(vec![SqlValue::Integer(2), SqlValue::Integer(10)], vec![3]),
(vec![SqlValue::Integer(2), SqlValue::Integer(15)], vec![4]),
]);
let results = index.skip_scan_range(
1,
Some(&SqlValue::Integer(10)),
false, Some(&SqlValue::Integer(20)),
false, );
assert_eq!(results.len(), 2);
assert!(results.contains(&1));
assert!(results.contains(&4));
}
#[test]
fn test_skip_scan_column_zero_falls_back() {
let index = create_test_index_data(vec![
(vec![SqlValue::Integer(10), SqlValue::Integer(1)], vec![0]),
(vec![SqlValue::Integer(20), SqlValue::Integer(2)], vec![1]),
]);
let results = index.skip_scan_equality(0, &SqlValue::Integer(10));
assert_eq!(results.len(), 1);
assert!(results.contains(&0));
}
#[test]
fn test_skip_scan_tpcc_like_scenario() {
let mut entries = Vec::new();
let mut row_idx = 0;
for w_id in 1..=3 {
for d_id in 1..=10 {
for o_id in 1..=5 {
let key =
vec![SqlValue::Integer(w_id), SqlValue::Integer(d_id), SqlValue::Integer(o_id)];
entries.push((key, vec![row_idx]));
row_idx += 1;
}
}
}
let index = create_test_index_data(entries);
let results = index.skip_scan_equality(1, &SqlValue::Integer(5));
assert_eq!(results.len(), 15);
for w_id in 1..=3 {
for o_id in 1..=5 {
let expected_row = ((w_id - 1) * 50 + (5 - 1) * 5 + (o_id - 1)) as usize;
assert!(
results.contains(&expected_row),
"Expected row {} for w_id={}, d_id=5, o_id={}",
expected_row,
w_id,
o_id
);
}
}
}