use super::*;
impl BTreeManager {
pub(super) async fn load_page_async(
&self,
page_id: u64,
async_coordinator: &AsyncFileCoordinator,
) -> Result<IndexPage, SqliteGraphError> {
if let Some(page) = self.page_cache.get(page_id) {
return Ok(page);
}
if page_id == 0 {
return Err(SqliteGraphError::connection(
"Cannot load page 0 (reserved for header)",
));
}
let initial_buf = vec![0u8; self.page_size as usize];
let (buffer, _) = async_coordinator.read_page(page_id, initial_buf).await?;
let page = IndexPage::unpack(&buffer).map_err(|e| {
SqliteGraphError::validation(format!("Failed to unpack index page: {}", e))
})?;
self.page_cache.insert(page_id, page.clone());
Ok(page)
}
pub(super) fn load_page(&self, page_id: u64) -> NativeResult<IndexPage> {
#[cfg(feature = "v3-forensics")]
FORENSIC_COUNTERS
.page_read_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if let Some(page) = self.page_cache.get(page_id) {
#[cfg(feature = "v3-forensics")]
FORENSIC_COUNTERS
.btree_cache_hit_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return Ok(page);
}
#[cfg(feature = "v3-forensics")]
FORENSIC_COUNTERS
.btree_cache_miss_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let db_path = match &self.db_path {
Some(path) => path,
None => {
return Err(NativeBackendError::InvalidHeader {
field: "page_cache".to_string(),
reason: format!("page {} not in cache (no disk path configured)", page_id),
});
}
};
if page_id == 0 {
return Err(NativeBackendError::InvalidHeader {
field: "page_id".to_string(),
reason: "Cannot load page 0 (reserved for header)".to_string(),
});
}
let mut buffer = vec![0u8; self.page_size as usize];
if let Some(coordinator) = &self.file_coordinator {
coordinator.read_page(page_id, &mut buffer)?;
} else {
let offset = V3_HEADER_SIZE + (page_id - 1) * self.page_size;
let mut file = File::open(db_path).map_err(|e| NativeBackendError::IoError {
context: format!("Failed to open db file for page load: {}", page_id),
source: e,
})?;
file.seek(SeekFrom::Start(offset))
.map_err(|e| NativeBackendError::IoError {
context: format!("Failed to seek to page {} at offset {}", page_id, offset),
source: e,
})?;
file.read_exact(&mut buffer)
.map_err(|e| NativeBackendError::IoError {
context: format!("Failed to read page {} from disk", page_id),
source: e,
})?;
}
let page = IndexPage::unpack(&buffer)?;
self.page_cache.insert(page_id, page.clone());
Ok(page)
}
pub(super) fn write_page(&mut self, page: &IndexPage) -> NativeResult<()> {
let page_id = page.page_id();
#[cfg(feature = "v3-forensics")]
{
FORENSIC_COUNTERS
.page_write_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let offset = V3_HEADER_SIZE + (page_id.saturating_sub(1)) * self.page_size;
let page_type = match page {
IndexPage::Leaf { .. } => PageType::BTree,
IndexPage::Internal { .. } => PageType::BTree,
};
crate::track_page_alloc!(page_id, Subsystem::BTreeManager, page_type);
crate::track_page_write!(
page_id,
Subsystem::BTreeManager,
page_type,
offset,
"BTreeManager::write_page"
);
}
let page_bytes = page.pack()?;
if self.db_path.is_some() {
if page_id == 0 {
return Err(NativeBackendError::InvalidHeader {
field: "page_id".to_string(),
reason: "Cannot write page 0 (reserved for header)".to_string(),
});
}
if let Some(coordinator) = &self.file_coordinator {
coordinator.write_page(page_id, &page_bytes)?;
} else {
let offset = V3_HEADER_SIZE + (page_id - 1) * self.page_size;
let required_len = offset + page_bytes.len() as u64;
let db_path =
self.db_path
.as_ref()
.ok_or_else(|| NativeBackendError::InvalidHeader {
field: "db_path".to_string(),
reason: "Cannot write page to disk without db_path".to_string(),
})?;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(db_path)
.map_err(|e| NativeBackendError::IoError {
context: format!("Failed to open db file for page write: {}", page_id),
source: e,
})?;
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 B+Tree page {}",
required_len, page_id
),
source: e,
})?;
file.sync_all().map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to sync file size to {} bytes for B+Tree page {}",
required_len, page_id
),
source: e,
})?;
}
file.seek(SeekFrom::Start(offset))
.map_err(|e| NativeBackendError::IoError {
context: format!("Failed to seek to page {} at offset {}", page_id, offset),
source: e,
})?;
file.write_all(&page_bytes)
.map_err(|e| NativeBackendError::IoError {
context: format!("Failed to write page {} to disk", page_id),
source: e,
})?;
file.sync_all().map_err(|e| NativeBackendError::IoError {
context: format!("Failed to sync B+Tree page {}", page_id),
source: e,
})?;
}
}
self.page_cache.insert(page_id, page.clone());
Ok(())
}
pub fn allocator(&self) -> parking_lot::RwLockReadGuard<'_, PageAllocator> {
self.allocator.read()
}
pub fn allocator_mut(&self) -> parking_lot::RwLockWriteGuard<'_, PageAllocator> {
self.allocator.write()
}
pub fn clear_cache(&mut self) {
self.page_cache.clear();
}
pub fn cache_stats(&self) -> (usize, usize) {
self.page_cache.stats()
}
pub fn page_cache(&self) -> BTreePageCache {
self.page_cache.clone()
}
pub fn create_write_batch(&self) -> WriteBatch {
WriteBatch::new()
}
pub fn stage_page_to_batch(&self, batch: &mut WriteBatch, page: IndexPage) -> NativeResult<()> {
batch.stage_page(page)
}
pub fn commit_batch(&mut self, batch: WriteBatch) -> NativeResult<()> {
let db_path =
self.db_path
.as_ref()
.ok_or_else(|| NativeBackendError::InvalidOperation {
context: "Cannot commit batch: no db_path configured".to_string(),
})?;
batch.commit(db_path)
}
pub fn insert_simple_to_batch(
&mut self,
batch: &mut WriteBatch,
key: i64,
value: u64,
) -> NativeResult<bool> {
if self.root_page_id == EMPTY_TREE_ROOT || self.root_page_id == 0 {
let page_id = self.allocator.write().allocate()?;
let mut leaf = IndexPage::new_leaf(page_id);
if let IndexPage::Leaf { entries, .. } = &mut leaf {
entries.push((key as u64, value));
}
batch.stage_page(leaf.clone())?;
self.page_cache.insert(page_id, leaf);
self.root_page_id = page_id;
self.tree_height = 1;
return Ok(true);
}
Err(NativeBackendError::InvalidOperation {
context: "Batch insert to non-empty tree not yet implemented".to_string(),
})
}
}