vibesql-storage 0.2.0

Storage engine for vibesql SQL database
Documentation
//! B+ Tree Index Implementation
//!
//! This module provides a disk-backed B+ tree index implementation using the page
//! management infrastructure. B+ trees are optimized for range queries and sequential
//! access patterns common in database workloads.
//!
//! # Architecture
//!
//! - **Internal Nodes**: Store keys and page pointers to child nodes
//! - **Leaf Nodes**: Store key-value pairs and pointers to next leaf (for range scans)
//! - **Page-based Storage**: All nodes are serialized to fixed-size pages (4KB)
//! - **Multi-column Keys**: Support composite keys via Vec<SqlValue>
//!
//! # Design Decisions
//!
//! - ADR-001: Reuse SqlValue serialization from persistence/binary/value.rs
//! - ADR-002: Page 0 reserved for B+ tree metadata (root_page_id, degree, height)
//! - ADR-003: Multi-column keys as Vec<SqlValue> for API compatibility
//! - ADR-004: Pure disk-backed implementation (buffer pool deferred to Part 3)
//!
//! # Page 0 Usage
//!
//! Each BTreeIndex instance manages its own database file via PageManager. Within each file:
//! - Page 0: B+ tree metadata (root_page_id, degree, height, key_schema)
//! - Page 1+: Internal and leaf nodes
//!
//! This design means page 0 is never a valid node page, making it a safe NULL sentinel.
//! Future work may allow multiple B+ trees to share a PageManager, in which case each
//! tree would need its own metadata page allocated via allocate_page().
//!
//! # Bulk Loading (Phase 5d - Part 5)
//!
//! The `BTreeIndex::bulk_load()` method provides optimized index creation from pre-sorted data.
//! This is significantly faster than incremental inserts for large datasets.
//!
//! ## Usage Example (once #1473 is integrated):
//!
//! ```text
//! // In CREATE INDEX executor:
//!
//! // 1. Collect and sort all rows by index key(s)
//! let mut sorted_entries = Vec::new();
//! for (row_idx, row) in table.rows().iter().enumerate() {
//!     let key: Vec<SqlValue> = column_indices
//!         .iter()
//!         .map(|&idx| row.values[idx].clone())
//!         .collect();
//!     sorted_entries.push((key, row_idx));
//! }
//! sorted_entries.sort_by(|a, b| a.0.cmp(&b.0));
//!
//! // 2. Build B+ tree using bulk loading
//! let key_schema: Vec<DataType> = column_indices
//!     .iter()
//!     .map(|&idx| table_schema.columns[idx].data_type.clone())
//!     .collect();
//!
//! let btree = BTreeIndex::bulk_load(
//!     sorted_entries,
//!     key_schema,
//!     page_manager.clone()
//! )?;
//! ```
//!
//! ## Performance Characteristics:
//!
//! | Table Size | Incremental | Bulk Load | Speedup |
//! |------------|-------------|-----------|---------|
//! | 1K rows    | 0.1s       | 0.1s      | 1x      |
//! | 10K rows   | 1s         | 0.5s      | 2x      |
//! | 100K rows  | 15s        | 2s        | 7.5x    |
//! | 1M rows    | 180s       | 15s       | 12x     |

mod node;
mod serialize;

pub use node::{BTreeIndex, Key, RowId};
use vibesql_types::DataType;

use crate::page::{PageId, PAGE_SIZE};

// Page type identifiers
#[allow(dead_code)] // Reserved for future use when implementing BTreeIndex::load()
const PAGE_TYPE_METADATA: u8 = 0;
const PAGE_TYPE_INTERNAL: u8 = 1;
const PAGE_TYPE_LEAF: u8 = 2;

// Null page reference
//
// NOTE: We use 0 as NULL_PAGE_ID because page 0 is reserved for metadata in each index file.
// Each BTreeIndex has its own database file, and within that file, page 0 stores the B+ tree's
// own metadata (root_page_id, degree, height, key_schema). Therefore, page 0 cannot be a valid
// child or next_leaf pointer, making it a safe NULL sentinel value.
const NULL_PAGE_ID: PageId = 0;

/// Calculate the maximum degree for a B+ tree based on key schema
///
/// The degree determines how many children an internal node can have
/// and how many entries a leaf node can store.
///
/// # Arguments
/// * `key_schema` - The data types of the key columns
///
/// # Returns
/// The maximum degree (minimum is enforced as 5)
fn calculate_degree(key_schema: &[DataType]) -> usize {
    let key_size = estimate_max_key_size(key_schema);

    // Internal node format: [header: 3 bytes][keys: N * key_size][children: (N+1) * 8 bytes]
    // Leaf node format: [header: 3 bytes][entries: N * (key_size + num_row_ids_varint +
    // row_ids)][next_leaf: 8 bytes]

    // Use leaf node calculation as it's more restrictive
    let header_size = 3; // page_type(1) + num_entries(2)
    let next_leaf_size = 8;
    // For single row_id entries (common case): key + num_row_ids(1 byte varint) + row_id(8 bytes)
    let entry_size = key_size + 1 + 8; // key + varint num_row_ids + row_id

    let available = PAGE_SIZE - header_size - next_leaf_size;
    let degree = available / entry_size;

    // Enforce minimum degree of 5 to maintain tree properties
    degree.max(5)
}

/// Estimate the maximum size of a serialized key
///
/// This includes the key length prefix (2 bytes) and all SqlValue data.
///
/// # Arguments
/// * `key_schema` - The data types of the key columns
///
/// # Returns
/// Estimated maximum size in bytes (including 2-byte length prefix)
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,                      // tag + i16
            DataType::Integer | DataType::Bigint => 1 + 8,    // tag + i64
            DataType::Unsigned => 1 + 8,                      // tag + u64
            DataType::Float { .. } | DataType::Real => 1 + 4, // tag + f32
            DataType::Numeric { .. } | DataType::Decimal { .. } | DataType::DoublePrecision => {
                1 + 8
            } // tag + f64
            DataType::Boolean => 1 + 1,                       // tag + bool
            DataType::Character { length } => {
                // tag + length(8) + max_chars
                // Conservative estimate: assume 4 bytes per UTF-8 char
                1 + 8 + (length * 4)
            }
            DataType::Varchar { max_length } => {
                // tag + length(8) + max_chars
                // Conservative estimate: assume 4 bytes per UTF-8 char
                let max_len = max_length.unwrap_or(255);
                1 + 8 + (max_len * 4)
            }
            DataType::CharacterLargeObject | DataType::Name => {
                // tag + length(8) + max_chars for NAME/CLOB
                // Conservative estimate: assume 4 bytes per UTF-8 char
                1 + 8 + (255 * 4)
            }
            DataType::Date => {
                // Serialized as strings, conservative estimate
                1 + 8 + 32
            }
            DataType::Time { .. } | DataType::Timestamp { .. } => {
                // Serialized as strings, conservative estimate
                1 + 8 + 32
            }
            DataType::Interval { .. } => {
                // Serialized as string, conservative estimate
                1 + 8 + 64
            }
            DataType::BinaryLargeObject => {
                // Conservative estimate for BLOB
                1 + 8 + 1024
            }
            DataType::Bit { length } => {
                // BIT type: tag + length + bits (rounded up to bytes)
                // MySQL BIT can be 1-64 bits
                let bit_length = length.unwrap_or(1);
                let byte_length = bit_length.div_ceil(8); // Round up to nearest byte
                1 + 8 + byte_length
            }
            DataType::Vector { dimensions } => {
                // tag + dimensions(4) + vector data (4 bytes per f32)
                1 + 4 + ((*dimensions as usize) * 4)
            }
            DataType::UserDefined { .. } => {
                // Unknown type, use conservative estimate
                1 + 8 + 256
            }
            DataType::Null => {
                // Just a tag
                1
            }
        };
        total_size += size;
    }

    // Add overhead for key length field (2 bytes) in serialization
    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);

        // For integer keys: 2 (key_len) + 1 (tag) + 8 (i64) = 11 bytes per key
        // Entry size: 11 + 8 (row_id) = 19 bytes
        // Available: 4096 - 3 (header) - 8 (next_leaf) = 4085 bytes
        // Degree: 4085 / 19 = 215
        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);

        // Should be significantly less than integer keys due to varchar size
        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);

        // Multi-column keys should have lower degree
        assert!(degree >= 5, "Expected degree >= 5, got {}", degree);
    }

    #[test]
    fn test_calculate_degree_minimum_enforced() {
        // Even with very large keys, minimum degree should be 5
        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");
    }

    /// Integration test: Bulk load vs incremental insert should produce equivalent trees
    ///
    /// This test will be used once #1473 (IndexManager integration) is complete
    /// to verify that bulk loading produces correct results
    #[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());

        // Create sorted test data
        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();

        // Build index using bulk load
        let index = BTreeIndex::bulk_load(sorted_entries, key_schema, page_manager).unwrap();

        // Verify basic properties
        assert!(index.height() >= 1);
        assert!(index.degree() >= 5);

        // TODO: Once search operations are implemented in #1473:
        // - Verify all keys are searchable
        // - Test range scans work correctly
        // - Compare results with incremental insert
    }
}