mod node;
mod serialize;
pub use node::{BTreeIndex, Key, RowId};
use vibesql_types::DataType;
use crate::page::{PageId, PAGE_SIZE};
#[allow(dead_code)] const PAGE_TYPE_METADATA: u8 = 0;
const PAGE_TYPE_INTERNAL: u8 = 1;
const PAGE_TYPE_LEAF: u8 = 2;
const NULL_PAGE_ID: PageId = 0;
fn calculate_degree(key_schema: &[DataType]) -> usize {
let key_size = estimate_max_key_size(key_schema);
let header_size = 3; let next_leaf_size = 8;
let entry_size = key_size + 1 + 8;
let available = PAGE_SIZE - header_size - next_leaf_size;
let degree = available / entry_size;
degree.max(5)
}
pub(crate) fn estimate_max_key_size(key_schema: &[DataType]) -> usize {
let mut total_size = 0;
for data_type in key_schema {
let size = match data_type {
DataType::Smallint => 1 + 2, DataType::Integer | DataType::Bigint => 1 + 8, DataType::Unsigned => 1 + 8, DataType::Float { .. } | DataType::Real => 1 + 4, DataType::Numeric { .. } | DataType::Decimal { .. } | DataType::DoublePrecision => {
1 + 8
} DataType::Boolean => 1 + 1, DataType::Character { length } => {
1 + 8 + (length * 4)
}
DataType::Varchar { max_length } => {
let max_len = max_length.unwrap_or(255);
1 + 8 + (max_len * 4)
}
DataType::CharacterLargeObject | DataType::Name => {
1 + 8 + (255 * 4)
}
DataType::Date => {
1 + 8 + 32
}
DataType::Time { .. } | DataType::Timestamp { .. } => {
1 + 8 + 32
}
DataType::Interval { .. } => {
1 + 8 + 64
}
DataType::BinaryLargeObject => {
1 + 8 + 1024
}
DataType::Bit { length } => {
let bit_length = length.unwrap_or(1);
let byte_length = bit_length.div_ceil(8); 1 + 8 + byte_length
}
DataType::Vector { dimensions } => {
1 + 4 + ((*dimensions as usize) * 4)
}
DataType::UserDefined { .. } => {
1 + 8 + 256
}
DataType::Null => {
1
}
};
total_size += size;
}
total_size + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_degree_integer_key() {
let key_schema = vec![DataType::Integer];
let degree = calculate_degree(&key_schema);
assert!(degree >= 200, "Expected degree >= 200, got {}", degree);
assert!(degree < 250, "Expected degree < 250, got {}", degree);
}
#[test]
fn test_calculate_degree_varchar_key() {
let key_schema = vec![DataType::Varchar { max_length: Some(50) }];
let degree = calculate_degree(&key_schema);
assert!(degree >= 5, "Expected degree >= 5, got {}", degree);
assert!(degree < 200, "Expected degree < 200 for varchar(50), got {}", degree);
}
#[test]
fn test_calculate_degree_multi_column() {
let key_schema = vec![DataType::Integer, DataType::Varchar { max_length: Some(20) }];
let degree = calculate_degree(&key_schema);
assert!(degree >= 5, "Expected degree >= 5, got {}", degree);
}
#[test]
fn test_calculate_degree_minimum_enforced() {
let key_schema = vec![DataType::Varchar { max_length: Some(10000) }];
let degree = calculate_degree(&key_schema);
assert_eq!(degree, 5, "Minimum degree of 5 should be enforced");
}
#[test]
fn test_bulk_load_produces_searchable_index() {
use std::sync::Arc;
use node::BTreeIndex;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(crate::NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(crate::page::PageManager::new("test.db", storage).unwrap());
let key_schema = vec![DataType::Integer];
let sorted_entries: Vec<(Vec<vibesql_types::SqlValue>, usize)> = (0..100)
.map(|i| (vec![vibesql_types::SqlValue::Integer(i * 10)], i as usize))
.collect();
let index = BTreeIndex::bulk_load(sorted_entries, key_schema, page_manager).unwrap();
assert!(index.height() >= 1);
assert!(index.degree() >= 5);
}
}