use std::convert::TryInto;
use std::marker::PhantomData;
use alopex_core::kv::KVTransaction;
use alopex_core::types::{Key, Value};
use super::error::{Result, StorageError};
use super::{KeyEncoder, SqlValue};
pub struct IndexStorage<'a, 'txn, T: KVTransaction<'txn>> {
txn: &'a mut T,
index_id: u32,
unique: bool,
column_indices: Vec<usize>,
_txn_lifetime: PhantomData<&'txn ()>,
}
impl<'a, 'txn, T: KVTransaction<'txn>> IndexStorage<'a, 'txn, T> {
pub fn new(txn: &'a mut T, index_id: u32, unique: bool, column_indices: Vec<usize>) -> Self {
Self {
txn,
index_id,
unique,
column_indices,
_txn_lifetime: PhantomData,
}
}
pub fn insert(&mut self, row: &[SqlValue], row_id: u64) -> Result<()> {
let values = self.extract_values(row)?;
let key = self.build_key(&values, row_id)?;
if self.unique {
let prefix = self.value_prefix(&values)?;
self.ensure_unique(&prefix, row_id)?;
}
self.txn.put(key, Vec::new())?;
Ok(())
}
pub fn delete(&mut self, row: &[SqlValue], row_id: u64) -> Result<()> {
let values = self.extract_values(row)?;
let key = self.build_key(&values, row_id)?;
self.txn.delete(key)?;
Ok(())
}
pub fn lookup(&mut self, value: &SqlValue) -> Result<Vec<u64>> {
if self.column_indices.len() != 1 {
return Err(StorageError::TypeMismatch {
expected: "single-column index".into(),
actual: format!("{} columns", self.column_indices.len()),
});
}
self.lookup_internal(std::slice::from_ref(value))
}
pub fn lookup_composite(&mut self, values: &[SqlValue]) -> Result<Vec<u64>> {
if values.len() != self.column_indices.len() {
return Err(StorageError::TypeMismatch {
expected: format!("{} values", self.column_indices.len()),
actual: format!("{} values", values.len()),
});
}
self.lookup_internal(values)
}
pub fn range_scan(
&mut self,
start: Option<&SqlValue>,
end: Option<&SqlValue>,
start_inclusive: bool,
end_inclusive: bool,
) -> Result<IndexScanIterator<'_>> {
if self.column_indices.len() != 1 {
return Err(StorageError::TypeMismatch {
expected: "single-column index".into(),
actual: format!("{} columns", self.column_indices.len()),
});
}
let (start_key, end_key) = self.range_bounds(start, end, start_inclusive, end_inclusive)?;
let index_id = self.index_id;
let inner = self.txn.scan_range(&start_key, &end_key)?;
Ok(IndexScanIterator::new(inner, index_id))
}
fn lookup_internal(&mut self, values: &[SqlValue]) -> Result<Vec<u64>> {
let prefix = self.value_prefix(values)?;
let iter = self.txn.scan_prefix(&prefix)?;
iter.map(|(key, _)| extract_row_id(&key, self.index_id))
.collect()
}
fn ensure_unique(&mut self, prefix: &[u8], row_id: u64) -> Result<()> {
let iter = self.txn.scan_prefix(prefix)?;
for (key, _) in iter {
let existing = extract_row_id(&key, self.index_id)?;
if existing != row_id {
return Err(StorageError::UniqueViolation {
index_id: self.index_id,
});
}
}
Ok(())
}
fn extract_values(&self, row: &[SqlValue]) -> Result<Vec<SqlValue>> {
let max_index = self.column_indices.iter().copied().max().unwrap_or(0);
if row.len() <= max_index {
return Err(StorageError::TypeMismatch {
expected: format!("row with >= {} columns", max_index + 1),
actual: format!("{} columns", row.len()),
});
}
Ok(self
.column_indices
.iter()
.map(|idx| row[*idx].clone())
.collect())
}
fn build_key(&self, values: &[SqlValue], row_id: u64) -> Result<Key> {
if self.column_indices.len() == 1 {
KeyEncoder::index_key(self.index_id, &values[0], row_id)
} else {
KeyEncoder::composite_index_key(self.index_id, values, row_id)
}
}
fn value_prefix(&self, values: &[SqlValue]) -> Result<Vec<u8>> {
if self.column_indices.len() == 1 {
KeyEncoder::index_value_prefix(self.index_id, &values[0])
} else {
KeyEncoder::composite_index_prefix(self.index_id, values)
}
}
fn range_bounds(
&self,
start: Option<&SqlValue>,
end: Option<&SqlValue>,
start_inclusive: bool,
end_inclusive: bool,
) -> Result<(Key, Key)> {
let start_key = match start {
Some(value) if start_inclusive => KeyEncoder::index_key(self.index_id, value, 0)?,
Some(value) => KeyEncoder::index_key(self.index_id, value, u64::MAX)?,
None => KeyEncoder::index_prefix(self.index_id),
};
let end_key = match end {
Some(value) if end_inclusive => {
let mut prefix = KeyEncoder::index_value_prefix(self.index_id, value)?;
prefix.extend_from_slice(&[0xFF; 9]);
prefix
}
Some(value) => KeyEncoder::index_key(self.index_id, value, 0)?,
None if self.index_id == u32::MAX => vec![0x03],
None => KeyEncoder::index_prefix(self.index_id.saturating_add(1)),
};
Ok((start_key, end_key))
}
}
fn extract_row_id(key: &[u8], expected_index: u32) -> Result<u64> {
if key.len() < 1 + 4 + 8 || key[0] != 0x02 {
return Err(StorageError::InvalidKeyFormat);
}
let index_id = u32::from_be_bytes(key[1..5].try_into().unwrap());
if index_id != expected_index {
return Err(StorageError::InvalidKeyFormat);
}
let row_id_pos = key.len().saturating_sub(8);
let mut buf = [0u8; 8];
buf.copy_from_slice(&key[row_id_pos..]);
Ok(u64::from_be_bytes(buf))
}
pub struct IndexScanIterator<'a> {
inner: Box<dyn Iterator<Item = (Key, Value)> + 'a>,
index_id: u32,
}
impl<'a> IndexScanIterator<'a> {
fn new(inner: Box<dyn Iterator<Item = (Key, Value)> + 'a>, index_id: u32) -> Self {
Self { inner, index_id }
}
}
impl<'a> Iterator for IndexScanIterator<'a> {
type Item = Result<u64>;
fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
.map(|(key, _)| extract_row_id(&key, self.index_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alopex_core::kv::KVStore;
use alopex_core::kv::memory::MemoryKV;
use alopex_core::types::TxnMode;
fn with_index<F>(unique: bool, column_indices: Vec<usize>, f: F)
where
F: FnOnce(&mut IndexStorage<'static, 'static, <MemoryKV as KVStore>::Transaction<'static>>),
{
let store = MemoryKV::new();
let store_static: &'static MemoryKV = Box::leak(Box::new(store));
let txn = store_static.begin(TxnMode::ReadWrite).unwrap();
let txn_static: &'static mut _ = Box::leak(Box::new(txn));
let mut index = IndexStorage::new(txn_static, 10, unique, column_indices);
f(&mut index);
}
#[test]
fn insert_lookup_and_delete_single_column() {
with_index(false, vec![0], |index| {
let row1 = vec![SqlValue::Integer(1), SqlValue::Text("a".into())];
let row2 = vec![SqlValue::Integer(2), SqlValue::Text("b".into())];
index.insert(&row1, 100).unwrap();
index.insert(&row2, 200).unwrap();
let mut results = index.lookup(&SqlValue::Integer(1)).unwrap();
results.sort();
assert_eq!(results, vec![100]);
let mut range_ids = {
let iter = index
.range_scan(
Some(&SqlValue::Integer(1)),
Some(&SqlValue::Integer(3)),
true,
false,
)
.unwrap();
iter.collect::<Result<Vec<_>>>().unwrap()
};
range_ids.sort();
assert_eq!(range_ids, vec![100, 200]);
index.delete(&row1, 100).unwrap();
assert!(index.lookup(&SqlValue::Integer(1)).unwrap().is_empty());
});
}
#[test]
fn unique_constraint_blocks_duplicates() {
with_index(true, vec![0], |index| {
let row = vec![SqlValue::Text("alice".into())];
index.insert(&row, 1).unwrap();
let err = index.insert(&row, 2).unwrap_err();
matches!(err, StorageError::UniqueViolation { .. });
});
}
#[test]
fn composite_lookup_returns_matching_row_ids() {
with_index(false, vec![0, 1], |index| {
let row1 = vec![SqlValue::Text("tokyo".into()), SqlValue::Integer(1)];
let row2 = vec![SqlValue::Text("tokyo".into()), SqlValue::Integer(2)];
let row3 = vec![SqlValue::Text("osaka".into()), SqlValue::Integer(1)];
index.insert(&row1, 10).unwrap();
index.insert(&row2, 20).unwrap();
index.insert(&row3, 30).unwrap();
let ids = index
.lookup_composite(&[SqlValue::Text("tokyo".into()), SqlValue::Integer(2)])
.unwrap();
assert_eq!(ids, vec![20]);
});
}
#[test]
fn range_scan_respects_inclusive_and_exclusive_bounds() {
with_index(false, vec![0], |index| {
for (i, val) in [1, 2, 3, 4, 5].iter().enumerate() {
let row = vec![SqlValue::Integer(*val)];
index.insert(&row, (i + 1) as u64).unwrap();
}
let ids = {
let iter = index
.range_scan(
Some(&SqlValue::Integer(2)),
Some(&SqlValue::Integer(4)),
false,
true,
)
.unwrap();
iter.collect::<Result<Vec<_>>>().unwrap()
};
assert_eq!(ids, vec![3, 4]);
});
}
#[test]
fn between_semantics_are_inclusive_on_both_ends() {
with_index(false, vec![0], |index| {
for (i, val) in [10, 20, 30, 40].iter().enumerate() {
let row = vec![SqlValue::Integer(*val)];
index.insert(&row, (i + 1) as u64).unwrap();
}
let ids = {
let iter = index
.range_scan(
Some(&SqlValue::Integer(20)),
Some(&SqlValue::Integer(40)),
true,
true,
)
.unwrap();
iter.collect::<Result<Vec<_>>>().unwrap()
};
assert_eq!(ids, vec![2, 3, 4]);
});
}
}