#[cfg(target_arch = "wasm32")]
use std::sync::Mutex;
use std::{collections::BTreeMap, sync::Arc};
#[cfg(not(target_arch = "wasm32"))]
use parking_lot::Mutex;
use vibesql_ast::IndexColumn;
use vibesql_types::SqlValue;
use super::{hnsw::HnswIndex, ivfflat::IVFFlatIndex};
use crate::{btree::BTreeIndex, page::PageManager, StorageError};
pub(super) fn normalize_index_name(name: &str) -> String {
name.to_lowercase()
}
pub(super) const DEFAULT_INDEX_SCHEMA: &str = "main";
pub(super) fn make_index_key(schema: &str, index_name: &str) -> String {
let normalized_name = normalize_index_name(index_name);
if schema.eq_ignore_ascii_case(DEFAULT_INDEX_SCHEMA) {
normalized_name
} else {
format!("{}.{}", schema.to_lowercase(), normalized_name)
}
}
#[cfg(all(not(test), not(feature = "in-memory-indexes")))]
pub(super) const DISK_BACKED_THRESHOLD: usize = 100_000;
#[cfg(any(test, feature = "in-memory-indexes"))]
pub(super) const DISK_BACKED_THRESHOLD: usize = usize::MAX;
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn acquire_btree_lock(
btree: &Arc<Mutex<BTreeIndex>>,
) -> Result<parking_lot::MutexGuard<'_, BTreeIndex>, StorageError> {
Ok(btree.lock())
}
#[cfg(target_arch = "wasm32")]
pub(super) fn acquire_btree_lock(
btree: &Arc<Mutex<BTreeIndex>>,
) -> Result<std::sync::MutexGuard<'_, BTreeIndex>, StorageError> {
btree.lock().map_err(|e| {
StorageError::LockError(format!(
"Failed to acquire BTreeIndex lock: mutex poisoned ({})",
e
))
})
}
#[derive(Debug, Clone)]
pub struct IndexMetadata {
pub index_name: String,
pub table_name: String,
pub schema: String,
pub unique: bool,
pub columns: Vec<IndexColumn>,
pub where_clause: Option<Box<vibesql_ast::Expression>>,
}
impl IndexMetadata {
#[inline]
pub fn is_partial(&self) -> bool {
self.where_clause.is_some()
}
}
#[derive(Debug, Clone)]
pub enum IndexData {
InMemory {
data: BTreeMap<Vec<SqlValue>, Vec<usize>>,
},
DiskBacked { btree: Arc<Mutex<BTreeIndex>>, page_manager: Arc<PageManager> },
IVFFlat { index: IVFFlatIndex },
Hnsw { index: HnswIndex },
}
impl IndexData {
pub fn first_key_value_sample(&self, col_idx: usize) -> Option<SqlValue> {
match self {
IndexData::InMemory { data, .. } => data
.keys()
.filter_map(|key| key.get(col_idx))
.find(|v| !matches!(v, SqlValue::Null))
.cloned(),
IndexData::DiskBacked { btree, .. } => {
use vibesql_types::DataType;
let guard = acquire_btree_lock(btree).ok()?;
let min_date = vibesql_types::Date { year: 1, month: 1, day: 1 };
let midnight = vibesql_types::Time { hour: 0, minute: 0, second: 0, nanosecond: 0 };
match guard.key_schema().get(col_idx)? {
DataType::Date => Some(SqlValue::Date(min_date)),
DataType::Timestamp { .. } => {
Some(SqlValue::Timestamp(vibesql_types::Timestamp::new(min_date, midnight)))
}
DataType::Time { .. } => Some(SqlValue::Time(midnight)),
_ => None,
}
}
_ => None,
}
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use std::str::FromStr;
use tempfile::TempDir;
use vibesql_types::DataType;
use super::*;
use crate::NativeStorage;
fn disk_backed_index(
key_schema: Vec<DataType>,
keys: Vec<Vec<SqlValue>>,
) -> (IndexData, TempDir) {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test_index.idx", storage).unwrap());
let mut sorted_entries: Vec<(Vec<SqlValue>, usize)> =
keys.into_iter().enumerate().map(|(i, key)| (key, i)).collect();
sorted_entries.sort_by(|a, b| a.0.cmp(&b.0));
let btree =
BTreeIndex::bulk_load(sorted_entries, key_schema, page_manager.clone()).unwrap();
let index = IndexData::DiskBacked { btree: Arc::new(Mutex::new(btree)), page_manager };
(index, temp_dir)
}
fn ts(s: &str) -> SqlValue {
SqlValue::Timestamp(vibesql_types::Timestamp::from_str(s).unwrap())
}
#[test]
fn disk_backed_sample_reports_timestamp_from_schema() {
let (index, _dir) = disk_backed_index(
vec![DataType::Timestamp { with_timezone: false }],
vec![vec![ts("2024-01-01 12:00:00")]],
);
assert!(matches!(index.first_key_value_sample(0), Some(SqlValue::Timestamp(_))));
}
#[test]
fn disk_backed_sample_reports_date_from_schema() {
let (index, _dir) = disk_backed_index(
vec![DataType::Date],
vec![vec![SqlValue::Date(vibesql_types::Date::from_str("2024-01-01").unwrap())]],
);
assert!(matches!(index.first_key_value_sample(0), Some(SqlValue::Date(_))));
}
#[test]
fn disk_backed_sample_reports_time_from_schema() {
let (index, _dir) = disk_backed_index(
vec![DataType::Time { with_timezone: false }],
vec![vec![SqlValue::Time(vibesql_types::Time::from_str("12:00:00").unwrap())]],
);
assert!(matches!(index.first_key_value_sample(0), Some(SqlValue::Time(_))));
}
#[test]
fn disk_backed_sample_returns_none_for_non_temporal_schema() {
let (index, _dir) =
disk_backed_index(vec![DataType::Integer], vec![vec![SqlValue::Integer(42)]]);
assert_eq!(index.first_key_value_sample(0), None);
}
#[test]
fn disk_backed_sample_works_for_empty_index() {
let (index, _dir) =
disk_backed_index(vec![DataType::Timestamp { with_timezone: false }], vec![]);
assert!(matches!(index.first_key_value_sample(0), Some(SqlValue::Timestamp(_))));
}
#[test]
fn disk_backed_sample_uses_per_column_schema() {
let (index, _dir) = disk_backed_index(
vec![DataType::Integer, DataType::Timestamp { with_timezone: false }],
vec![vec![SqlValue::Integer(1), ts("2024-01-01 12:00:00")]],
);
assert_eq!(index.first_key_value_sample(0), None);
assert!(matches!(index.first_key_value_sample(1), Some(SqlValue::Timestamp(_))));
assert_eq!(index.first_key_value_sample(2), None);
}
}