use crate::backend::native::NativeBackendError;
use crate::backend::native::NativeResult;
use crate::backend::native::types::NodeFlags;
use crate::backend::native::v3::allocator::PageAllocator;
use crate::backend::native::v3::btree::BTreeManager;
use crate::backend::native::v3::constants::{DEFAULT_PAGE_SIZE, V3_HEADER_SIZE};
use crate::backend::native::v3::file_coordinator::FileCoordinator;
#[cfg(feature = "v3-forensics")]
use crate::backend::native::v3::forensics::{FORENSIC_COUNTERS, PageType, Subsystem};
use crate::backend::native::v3::header::PersistentHeaderV3;
use crate::backend::native::v3::index::IndexPage;
use crate::backend::native::v3::node::{NodePage, NodeRecordV3};
use crate::backend::native::v3::wal::WALWriter;
use lru::LruCache;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;
mod async_lookup;
mod lookup_support;
mod mutation_support;
mod page_loader;
#[cfg(test)]
mod tests;
mod traversal_cache;
pub use async_lookup::lookup_node_async;
pub use page_loader::PageLoader;
pub use traversal_cache::{
DEFAULT_CACHE_CAPACITY, MAX_CACHE_CAPACITY, MIN_CACHE_CAPACITY, TraversalCache,
TraversalCacheBuilder,
};
const MAX_TREE_HEIGHT: u32 = 10;
pub const PAGE_CACHE_SIZE: usize = 1024;
fn new_page_cache<K: std::hash::Hash + Eq, V>() -> LruCache<K, V> {
LruCache::new(NonZeroUsize::new(PAGE_CACHE_SIZE).expect("PAGE_CACHE_SIZE > 0"))
}
fn new_lru<K: std::hash::Hash + Eq, V>(capacity: usize) -> LruCache<K, V> {
LruCache::new(NonZeroUsize::new(capacity.max(1)).expect("capacity must be >= 1"))
}
pub struct NodeStore {
db_path: PathBuf,
file_coordinator: Option<Arc<FileCoordinator>>,
root_page_id: u64,
tree_height: u32,
page_cache: Arc<RwLock<LruCache<u64, Vec<u8>>>>,
unpacked_page_cache: Arc<RwLock<LruCache<u64, Arc<NodePage>>>>,
cache_capacity: usize,
current_access_block: std::sync::atomic::AtomicI64,
block_preferred_pages: HashMap<i64, Vec<u64>>,
max_preferred_pages_per_block: usize,
index_cache: LruCache<u64, IndexPage>,
btree_manager: Option<BTreeManager>,
page_allocator: Option<Arc<RwLock<PageAllocator>>>,
wal_writer: Option<WALWriter>,
next_node_id: i64,
dirty_pages: HashMap<u64, NodePage>,
batch_mode: bool,
}
impl NodeStore {
pub fn new(header: &PersistentHeaderV3, db_path: PathBuf) -> Self {
NodeStore {
db_path,
file_coordinator: None,
root_page_id: header.root_index_page,
tree_height: header.btree_height,
page_cache: Arc::new(RwLock::new(new_page_cache())),
unpacked_page_cache: Arc::new(RwLock::new(new_page_cache())),
cache_capacity: PAGE_CACHE_SIZE,
current_access_block: std::sync::atomic::AtomicI64::new(-1),
block_preferred_pages: HashMap::new(),
max_preferred_pages_per_block: 3,
index_cache: new_page_cache(),
btree_manager: None,
page_allocator: None,
wal_writer: None,
next_node_id: 1, dirty_pages: HashMap::new(),
batch_mode: false,
}
}
pub fn btree_manager(&self) -> &Option<BTreeManager> {
&self.btree_manager
}
pub fn page_cache_ref(&self) -> &Arc<RwLock<LruCache<u64, Vec<u8>>>> {
&self.page_cache
}
pub fn unpacked_page_cache_ref(&self) -> &Arc<RwLock<LruCache<u64, Arc<NodePage>>>> {
&self.unpacked_page_cache
}
pub fn cache_capacity(&self) -> usize {
self.cache_capacity
}
pub fn page_cache_len(&self) -> usize {
self.page_cache.read().len()
}
pub fn unpacked_page_cache_len(&self) -> usize {
self.unpacked_page_cache.read().len()
}
pub fn page_cache_contains(&self, page_id: u64) -> bool {
self.page_cache.read().peek(&page_id).is_some()
}
pub fn page_id_for_node(&self, node_id: i64) -> NativeResult<Option<u64>> {
self.lookup_page_ro(node_id)
}
pub fn clear_all_caches(&mut self) {
self.page_cache.write().clear();
self.unpacked_page_cache.write().clear();
self.index_cache.clear();
}
pub fn with_capacity(
header: &PersistentHeaderV3,
db_path: PathBuf,
cache_capacity: usize,
) -> Self {
NodeStore {
db_path,
file_coordinator: None,
root_page_id: header.root_index_page,
tree_height: header.btree_height,
page_cache: Arc::new(RwLock::new(new_lru(cache_capacity))),
unpacked_page_cache: Arc::new(RwLock::new(new_lru(cache_capacity))),
cache_capacity,
current_access_block: std::sync::atomic::AtomicI64::new(-1),
block_preferred_pages: HashMap::new(),
max_preferred_pages_per_block: 3,
index_cache: new_lru(cache_capacity),
btree_manager: None,
page_allocator: None,
wal_writer: None,
next_node_id: 1,
dirty_pages: HashMap::new(),
batch_mode: false,
}
}
pub fn initialize(
&mut self,
btree_manager: BTreeManager,
page_allocator: Arc<RwLock<PageAllocator>>,
wal_writer: Option<WALWriter>,
) {
self.btree_manager = Some(btree_manager);
self.page_allocator = Some(page_allocator);
self.wal_writer = wal_writer;
}
pub fn set_wal_writer(&mut self, wal: WALWriter) {
self.wal_writer = Some(wal);
}
pub fn set_file_coordinator(&mut self, coordinator: Arc<FileCoordinator>) {
if let Some(ref mut btree) = self.btree_manager {
btree.set_file_coordinator(Arc::clone(&coordinator));
}
self.file_coordinator = Some(coordinator);
}
pub fn begin_batch(&mut self) {
self.batch_mode = true;
self.dirty_pages.clear();
}
pub fn commit_batch(&mut self) -> NativeResult<usize> {
if !self.batch_mode {
return Ok(0);
}
let page_count = self.dirty_pages.len();
if page_count > 0 {
if let Some(coordinator) = &self.file_coordinator {
for (page_id, page) in &self.dirty_pages {
let page_bytes = page.pack()?;
coordinator.write_page(*page_id, &page_bytes)?;
self.page_cache_insert(*page_id, page_bytes.to_vec());
}
self.dirty_pages.clear();
} else {
let file_exists = self.db_path.exists();
let mut file = OpenOptions::new()
.write(true)
.create(!file_exists) .open(&self.db_path)
.map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to open database file for batch commit: {}",
self.db_path.display()
),
source: e,
})?;
let mut required_len = file.metadata().map(|m| m.len()).unwrap_or(0);
for (page_id, page) in &self.dirty_pages {
let page_bytes = page.pack()?;
let offset = Self::page_offset(*page_id);
let page_end = offset + page_bytes.len() as u64;
if page_end > required_len {
required_len = page_end;
}
}
let current_len = file.metadata().map(|m| m.len()).unwrap_or(0);
if required_len > current_len {
file.set_len(required_len)
.map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to extend file to {} bytes for batch commit",
required_len
),
source: e,
})?;
}
for (page_id, page) in &self.dirty_pages {
let page_bytes = page.pack()?;
let offset = Self::page_offset(*page_id);
file.seek(SeekFrom::Start(offset)).map_err(|e| {
NativeBackendError::IoError {
context: format!(
"Failed to seek to page {} offset {}",
page_id, offset
),
source: e,
}
})?;
file.write_all(&page_bytes)
.map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to write page {} during batch commit",
page_id
),
source: e,
})?;
self.page_cache_insert(*page_id, page_bytes.to_vec());
}
file.sync_all().map_err(|e| NativeBackendError::IoError {
context: "Failed to sync batch commit to disk".to_string(),
source: e,
})?;
self.dirty_pages.clear();
}
}
self.batch_mode = false;
Ok(page_count)
}
pub fn rollback_batch(&mut self) {
self.dirty_pages.clear();
self.batch_mode = false;
}
pub fn is_batch_mode(&self) -> bool {
self.batch_mode
}
pub fn dirty_page_count(&self) -> usize {
self.dirty_pages.len()
}
fn btree_manager_mut(&mut self) -> NativeResult<&mut BTreeManager> {
self.btree_manager
.as_mut()
.ok_or_else(|| NativeBackendError::InvalidHeader {
field: "btree_manager".to_string(),
reason: "BTreeManager not initialized".to_string(),
})
}
fn page_allocator_mut(&self) -> NativeResult<parking_lot::RwLockWriteGuard<'_, PageAllocator>> {
self.page_allocator
.as_ref()
.ok_or_else(|| NativeBackendError::InvalidHeader {
field: "page_allocator".to_string(),
reason: "PageAllocator not initialized".to_string(),
})
.map(|arc| arc.write())
}
}