use parking_lot::RwLock;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::bounding_box::BoundingBox;
use crate::nitrite_rtree::NitriteRTree;
use super::rtree_types::{
SpatialError, SpatialResult, NitriteIdValue, RTreeStats, RebuildStats, FragmentationMetrics,
InternalBBox, Node, LeafEntry, ChildRef, FileHeader, PageId,
};
use super::rtree_cache::PageCache;
use super::rtree_storage::Storage;
use super::rtree_constants::{DEFAULT_CACHE_PAGES, MAX_LEAF_ENTRIES, MAX_INTERNAL_CHILDREN};
use super::persistence::{IntegrityReport, RepairOptions, RepairReport, MigrationManager};
pub struct DiskRTree {
inner: std::sync::Arc<DiskRTreeInner>,
}
struct DiskRTreeInner {
storage: Storage,
cache: RwLock<PageCache>,
header: RwLock<FileHeader>,
stats: RTreeStatistics,
closed: RwLock<bool>,
free_pages: RwLock<Vec<PageId>>,
}
struct RTreeStatistics {
cache_hits: AtomicU64,
cache_misses: AtomicU64,
disk_reads: AtomicU64,
disk_writes: AtomicU64,
}
impl RTreeStatistics {
fn new() -> Self {
Self {
cache_hits: AtomicU64::new(0),
cache_misses: AtomicU64::new(0),
disk_reads: AtomicU64::new(0),
disk_writes: AtomicU64::new(0),
}
}
}
impl DiskRTree {
pub fn create(path: impl AsRef<Path>) -> SpatialResult<Self> {
Self::create_with_cache_size(path, DEFAULT_CACHE_PAGES)
}
pub fn create_with_cache_size(
path: impl AsRef<Path>,
cache_pages: usize,
) -> SpatialResult<Self> {
let storage = Storage::create(path.as_ref())?;
let header = FileHeader::new();
storage.write_header(&header)?;
storage.sync()?;
Ok(Self {
inner: std::sync::Arc::new(DiskRTreeInner {
storage,
cache: RwLock::new(PageCache::new(cache_pages)),
header: RwLock::new(header),
stats: RTreeStatistics::new(),
closed: RwLock::new(false),
free_pages: RwLock::new(Vec::new()),
}),
})
}
pub fn open(path: impl AsRef<Path>) -> SpatialResult<Self> {
Self::open_with_cache_size(path, DEFAULT_CACHE_PAGES)
}
pub fn open_with_cache_size(
path: impl AsRef<Path>,
cache_pages: usize,
) -> SpatialResult<Self> {
let storage = Storage::open(path.as_ref())?;
let header = storage.read_header()?;
header.validate()?;
Ok(Self {
inner: std::sync::Arc::new(DiskRTreeInner {
storage,
cache: RwLock::new(PageCache::new(cache_pages)),
header: RwLock::new(header),
stats: RTreeStatistics::new(),
closed: RwLock::new(false),
free_pages: RwLock::new(Vec::new()),
}),
})
}
pub fn bulk_load<I>(
path: impl AsRef<Path>,
entries: I,
) -> SpatialResult<Self>
where
I: IntoIterator<Item = (BoundingBox, NitriteIdValue)>,
{
Self::bulk_load_with_cache_size(path, DEFAULT_CACHE_PAGES, entries)
}
pub fn bulk_load_with_cache_size<I>(
path: impl AsRef<Path>,
cache_pages: usize,
entries: I,
) -> SpatialResult<Self>
where
I: IntoIterator<Item = (BoundingBox, NitriteIdValue)>,
{
use crate::hilbert::hilbert_index_bounded;
let tree = Self::create_with_cache_size(&path, cache_pages)?;
let mut indexed_entries: Vec<_> = entries
.into_iter()
.map(|(bbox, id)| {
let cx = (bbox.min_x + bbox.max_x) / 2.0;
let cy = (bbox.min_y + bbox.max_y) / 2.0;
let h_index = hilbert_index_bounded(cx, cy, &bbox, 16);
(h_index, bbox, id)
})
.collect();
indexed_entries.sort_by_key(|entry| entry.0);
for (_, bbox, id) in indexed_entries {
tree.add(&bbox, id)?;
}
Ok(tree)
}
fn check_closed(&self) -> SpatialResult<()> {
if *self.inner.closed.read() {
Err(SpatialError::Closed)
} else {
Ok(())
}
}
pub fn stats(&self) -> RTreeStats {
let header = self.inner.header.read();
let cache = self.inner.cache.read();
RTreeStats {
total_entries: header.entry_count,
cached_pages: cache.len() as u64,
cache_hits: self.inner.stats.cache_hits.load(Ordering::Relaxed),
cache_misses: self.inner.stats.cache_misses.load(Ordering::Relaxed),
disk_reads: self.inner.stats.disk_reads.load(Ordering::Relaxed),
disk_writes: self.inner.stats.disk_writes.load(Ordering::Relaxed),
tree_height: header.height,
}
}
fn collect_all_entries(&self) -> SpatialResult<Vec<(InternalBBox, NitriteIdValue)>> {
let root_page = self.inner.header.read().root_page;
if root_page == 0 {
return Ok(Vec::new());
}
let mut entries = Vec::new();
self.collect_entries_recursive(root_page, &mut entries)?;
Ok(entries)
}
fn collect_entries_recursive(
&self,
page_id: PageId,
entries: &mut Vec<(InternalBBox, NitriteIdValue)>,
) -> SpatialResult<()> {
let node = self.read_node(page_id)?;
match node {
Node::Leaf { entries: leaf_entries } => {
for entry in leaf_entries {
entries.push((entry.bbox, entry.id));
}
}
Node::Internal { children, .. } => {
for child in children {
self.collect_entries_recursive(child.page_id, entries)?;
}
}
}
Ok(())
}
pub fn rebuild(&self) -> SpatialResult<RebuildStats> {
self.check_closed()?;
let stats_before = self.stats();
let entries_before = stats_before.total_entries;
let pages_before = stats_before.cached_pages;
let height_before = stats_before.tree_height;
let all_entries = self.collect_all_entries()?;
self.clear()?;
for (bbox, id) in all_entries {
let public_bbox = BoundingBox::new(bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y);
self.add(&public_bbox, id)?;
}
let stats_after = self.stats();
let pages_after = stats_after.cached_pages;
let height_after = stats_after.tree_height;
Ok(RebuildStats {
entries_reindexed: entries_before,
pages_before,
pages_after,
height_before,
height_after,
fill_factor_improvement: 0.0,
})
}
pub fn detect_fragmentation(&self) -> SpatialResult<FragmentationMetrics> {
self.check_closed()?;
let stats = self.stats();
Ok(FragmentationMetrics::calculate(&stats, stats.total_entries))
}
pub fn rebuild_if_fragmented(&self) -> SpatialResult<(FragmentationMetrics, Option<RebuildStats>)> {
self.check_closed()?;
let metrics = self.detect_fragmentation()?;
if metrics.should_rebuild {
let rebuild_stats = self.rebuild()?;
Ok((metrics, Some(rebuild_stats)))
} else {
Ok((metrics, None))
}
}
pub fn flush(&self) -> SpatialResult<()> {
let dirty_pages = self.inner.cache.read().get_dirty_pages();
for page_id in dirty_pages {
let mut cache = self.inner.cache.write();
if let Some(cached) = cache.pages.get(&page_id) {
if cached.dirty {
self.inner.storage.write_page(page_id, &cached.node)?;
self.inner.stats.disk_writes.fetch_add(1, Ordering::Relaxed);
cache.mark_clean(page_id);
}
}
}
self.inner.storage.write_header(&self.inner.header.read())?;
self.inner.storage.sync()?;
Ok(())
}
pub fn check_integrity(&self) -> SpatialResult<IntegrityReport> {
self.check_closed()?;
let mut report = IntegrityReport::new();
let header = self.inner.header.read();
if let Err(e) = header.validate() {
report.errors.push(format!("Invalid header: {}", e));
report.is_valid = false;
return Ok(report);
}
if header.root_page != 0 {
match self.inner.storage.read_page(header.root_page) {
Ok(_node) => {
report.pages_checked += 1;
}
Err(e) => {
if e.to_string().contains("checksum") {
report.corrupted_pages.push(header.root_page);
report.errors.push(format!("Page {}: {}", header.root_page, e));
report.is_valid = false;
}
}
}
}
let mut current_page_id = 1;
let next_page_id = header.next_page_id;
while current_page_id < next_page_id {
match self.inner.storage.read_page(current_page_id) {
Ok(_node) => {
report.pages_checked += 1;
}
Err(e) => {
if e.to_string().contains("checksum") || e.to_string().contains("corruption") {
report.corrupted_pages.push(current_page_id);
report.errors.push(format!("Page {}: {}", current_page_id, e));
report.is_valid = false;
}
}
}
current_page_id += 1;
}
Ok(report)
}
pub fn repair(&self, options: RepairOptions) -> SpatialResult<RepairReport> {
self.check_closed()?;
let mut report = RepairReport::new();
let integrity = self.check_integrity()?;
if !integrity.corrupted_pages.is_empty()
&& options.remove_corrupt {
for _page_id in &integrity.corrupted_pages {
if let Some(max_repairs) = options.max_repairs {
if report.pages_removed >= max_repairs {
break;
}
}
report.pages_removed += 1;
}
}
if options.rebuild_if_needed && !integrity.is_valid {
match self.rebuild() {
Ok(_stats) => {
report.rebuild_performed = true;
}
Err(e) => {
report.errors.push(format!("Rebuild failed: {}", e));
}
}
}
Ok(report)
}
pub fn check_and_migrate(&self) -> SpatialResult<()> {
let header = self.inner.header.read().clone();
if MigrationManager::needs_migration(&header) {
println!(
"File format needs migration from version {} to {}",
header.version,
MigrationManager::current_version()
);
MigrationManager::migrate(&self.inner.storage, &mut self.inner.header.write())?;
println!("Migration complete");
}
Ok(())
}
fn allocate_page(&self) -> PageId {
{
let mut free_pages = self.inner.free_pages.write();
if let Some(page_id) = free_pages.pop() {
return page_id;
}
}
let mut header = self.inner.header.write();
if header.free_list_head != 0 {
let page_id = header.free_list_head;
header.free_list_head = 0;
return page_id;
}
let page_id = header.next_page_id;
header.next_page_id += 1;
page_id
}
fn free_page(&self, page_id: PageId) {
let mut free_pages = self.inner.free_pages.write();
free_pages.push(page_id);
if free_pages.len() == 1 && page_id > 0 {
let mut header = self.inner.header.write();
header.free_list_head = page_id;
}
}
fn read_node(&self, page_id: PageId) -> SpatialResult<Node> {
{
let mut cache = self.inner.cache.write();
if let Some(node) = cache.get(page_id) {
self.inner.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
return Ok(node.clone());
}
}
self.inner.stats.cache_misses.fetch_add(1, Ordering::Relaxed);
self.inner.stats.disk_reads.fetch_add(1, Ordering::Relaxed);
let node = self.inner.storage.read_page(page_id)?;
self.cache_node(page_id, node.clone(), false)?;
Ok(node)
}
fn write_node(&self, page_id: PageId, node: Node) -> SpatialResult<()> {
self.cache_node(page_id, node, true)
}
fn cache_node(&self, page_id: PageId, node: Node, dirty: bool) -> SpatialResult<()> {
let mut cache = self.inner.cache.write();
while cache.needs_eviction() {
if let Some((evict_id, evict_node, evict_dirty)) = cache.evict_oldest() {
if evict_dirty {
self.inner.storage.write_page(evict_id, &evict_node)?;
self.inner.stats.disk_writes.fetch_add(1, Ordering::Relaxed);
}
} else {
break;
}
}
cache.insert(page_id, node, dirty);
Ok(())
}
fn choose_leaf(
&self,
page_id: PageId,
bbox: &InternalBBox,
path: &mut Vec<(PageId, usize)>,
) -> SpatialResult<PageId> {
let node = self.read_node(page_id)?;
match node {
Node::Leaf { .. } => Ok(page_id),
Node::Internal { children, .. } => {
let mut best_idx = 0;
let mut best_enlargement = f64::INFINITY;
let mut best_area = f64::INFINITY;
for (i, child) in children.iter().enumerate() {
let enlargement = child.bbox.enlargement(bbox);
let area = child.bbox.area();
if enlargement < best_enlargement
|| (enlargement == best_enlargement && area < best_area)
{
best_enlargement = enlargement;
best_area = area;
best_idx = i;
}
}
path.push((page_id, best_idx));
self.choose_leaf(children[best_idx].page_id, bbox, path)
}
}
}
fn insert_into_leaf(
&self,
page_id: PageId,
entry: LeafEntry,
) -> SpatialResult<Option<(PageId, InternalBBox)>> {
let mut node = self.read_node(page_id)?;
if let Node::Leaf { ref mut entries } = node {
entries.push(entry);
if entries.len() > MAX_LEAF_ENTRIES {
let (remaining, new_entries) = self.split_leaf(entries);
*entries = remaining;
let new_page_id = self.allocate_page();
let new_bbox = compute_entries_bbox(&new_entries);
let new_node = Node::Leaf {
entries: new_entries,
};
self.write_node(page_id, node)?;
self.write_node(new_page_id, new_node)?;
return Ok(Some((new_page_id, new_bbox)));
}
self.write_node(page_id, node)?;
Ok(None)
} else {
Err(SpatialError::InvalidOperation(
"Expected leaf node for insertion".into(),
))
}
}
fn split_leaf(&self, entries: &[LeafEntry]) -> (Vec<LeafEntry>, Vec<LeafEntry>) {
let mut sorted: Vec<_> = entries.to_vec();
sorted.sort_by(|a, b| {
let center_a = (a.bbox.min_x + a.bbox.max_x) / 2.0;
let center_b = (b.bbox.min_x + b.bbox.max_x) / 2.0;
center_a
.partial_cmp(¢er_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mid = sorted.len() / 2;
(sorted[..mid].to_vec(), sorted[mid..].to_vec())
}
fn propagate_split(
&self,
path: &[(PageId, usize)],
mut new_page: PageId,
mut new_bbox: InternalBBox,
) -> SpatialResult<()> {
for &(parent_id, child_idx) in path.iter().rev() {
let mut parent_node = self.read_node(parent_id)?;
if let Node::Internal {
ref mut children,
level,
} = parent_node
{
children[child_idx].bbox =
self.read_node(children[child_idx].page_id)?.compute_bbox();
children.push(ChildRef {
bbox: new_bbox,
page_id: new_page,
});
if children.len() > MAX_INTERNAL_CHILDREN {
let (remaining, new_children) = self.split_internal(children);
*children = remaining;
new_page = self.allocate_page();
new_bbox = compute_children_bbox(&new_children);
let new_node = Node::Internal {
children: new_children,
level,
};
self.write_node(parent_id, parent_node)?;
self.write_node(new_page, new_node)?;
} else {
self.write_node(parent_id, parent_node)?;
return Ok(());
}
}
}
let old_root = self.inner.header.read().root_page;
let old_root_bbox = self.read_node(old_root)?.compute_bbox();
let new_root_id = self.allocate_page();
let new_root = Node::Internal {
children: vec![
ChildRef {
bbox: old_root_bbox,
page_id: old_root,
},
ChildRef {
bbox: new_bbox,
page_id: new_page,
},
],
level: self.inner.header.read().height,
};
self.write_node(new_root_id, new_root)?;
let mut header = self.inner.header.write();
header.root_page = new_root_id;
header.height += 1;
Ok(())
}
fn split_internal(&self, children: &[ChildRef]) -> (Vec<ChildRef>, Vec<ChildRef>) {
let mut sorted: Vec<_> = children.to_vec();
sorted.sort_by(|a, b| {
let center_a = (a.bbox.min_x + a.bbox.max_x) / 2.0;
let center_b = (b.bbox.min_x + b.bbox.max_x) / 2.0;
center_a
.partial_cmp(¢er_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mid = sorted.len() / 2;
(sorted[..mid].to_vec(), sorted[mid..].to_vec())
}
fn update_path_bboxes(&self, path: &[(PageId, usize)]) -> SpatialResult<()> {
for &(parent_id, child_idx) in path.iter().rev() {
let mut parent_node = self.read_node(parent_id)?;
if let Node::Internal {
ref mut children, ..
} = parent_node
{
let child_bbox = self.read_node(children[child_idx].page_id)?.compute_bbox();
children[child_idx].bbox = child_bbox;
self.write_node(parent_id, parent_node)?;
}
}
Ok(())
}
fn search_recursive(
&self,
page_id: PageId,
query: &InternalBBox,
results: &mut Vec<NitriteIdValue>,
) -> SpatialResult<()> {
let node = self.read_node(page_id)?;
match node {
Node::Leaf { entries } => {
for entry in entries {
if entry.bbox.intersects(query) {
results.push(entry.id);
}
}
}
Node::Internal { children, .. } => {
for child in children {
if child.bbox.intersects(query) {
self.search_recursive(child.page_id, query, results)?;
}
}
}
}
Ok(())
}
fn search_contained_recursive(
&self,
page_id: PageId,
query: &InternalBBox,
results: &mut Vec<NitriteIdValue>,
) -> SpatialResult<()> {
let node = self.read_node(page_id)?;
match node {
Node::Leaf { entries } => {
for entry in entries {
if query.contains(&entry.bbox) {
results.push(entry.id);
}
}
}
Node::Internal { children, .. } => {
for child in children {
if child.bbox.intersects(query) {
self.search_contained_recursive(child.page_id, query, results)?;
}
}
}
}
Ok(())
}
fn remove_recursive(
&self,
page_id: PageId,
bbox: &InternalBBox,
id: NitriteIdValue,
) -> SpatialResult<bool> {
let mut node = self.read_node(page_id)?;
match &mut node {
Node::Leaf { entries } => {
let original_len = entries.len();
entries.retain(|e| !(e.bbox == *bbox && e.id == id));
if entries.len() < original_len {
self.write_node(page_id, node)?;
return Ok(true);
}
Ok(false)
}
Node::Internal { children, .. } => {
for i in 0..children.len() {
if children[i].bbox.intersects(bbox)
&& self.remove_recursive(children[i].page_id, bbox, id)? {
let child_node = self.read_node(children[i].page_id)?;
let new_bbox = child_node.compute_bbox();
if child_node.is_underfull() && child_node.is_empty() {
self.free_page(children[i].page_id);
children.remove(i);
} else {
children[i].bbox = new_bbox;
}
self.write_node(page_id, node)?;
return Ok(true);
}
}
Ok(false)
}
}
}
pub fn find_nearest(
&self,
center_x: f64,
center_y: f64,
k: usize,
max_distance: Option<f64>,
) -> SpatialResult<Vec<(NitriteIdValue, f64)>> {
self.check_closed()?;
if k == 0 {
return Ok(Vec::new());
}
let root_page = self.inner.header.read().root_page;
if root_page == 0 {
return Ok(Vec::new());
}
let mut results: Vec<(NitriteIdValue, f64)> = Vec::new();
let mut max_dist_so_far = max_distance.unwrap_or(f64::INFINITY);
self.find_nearest_recursive(
root_page,
center_x,
center_y,
k,
&mut results,
&mut max_dist_so_far,
)?;
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(k);
Ok(results)
}
fn find_nearest_recursive(
&self,
page_id: PageId,
center_x: f64,
center_y: f64,
k: usize,
results: &mut Vec<(NitriteIdValue, f64)>,
max_dist: &mut f64,
) -> SpatialResult<()> {
let node = self.read_node(page_id)?;
match node {
Node::Leaf { entries } => {
for entry in entries {
let dist = self.point_to_bbox_distance(center_x, center_y, &entry.bbox);
if dist <= *max_dist {
results.push((entry.id, dist));
if results.len() > k {
results.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(k);
if let Some((_, kth_dist)) = results.last() {
*max_dist = *kth_dist;
}
}
}
}
}
Node::Internal { children, .. } => {
let mut candidates: Vec<_> = children
.iter()
.map(|child| {
let dist = self.point_to_bbox_distance(center_x, center_y, &child.bbox);
(child.clone(), dist)
})
.collect();
candidates.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
});
for (child, dist) in candidates {
if dist > *max_dist {
continue;
}
self.find_nearest_recursive(
child.page_id,
center_x,
center_y,
k,
results,
max_dist,
)?;
}
}
}
Ok(())
}
fn point_to_bbox_distance(&self, px: f64, py: f64, bbox: &InternalBBox) -> f64 {
let closest_x = px.clamp(bbox.min_x, bbox.max_x);
let closest_y = py.clamp(bbox.min_y, bbox.max_y);
let dx = px - closest_x;
let dy = py - closest_y;
(dx * dx + dy * dy).sqrt()
}
}
impl NitriteRTree for DiskRTree {
fn add(&self, key: &BoundingBox, nitrite_id: NitriteIdValue) -> SpatialResult<()> {
self.check_closed()?;
let bbox = InternalBBox::from_bbox(key);
let entry = LeafEntry { bbox, id: nitrite_id };
let root_page = self.inner.header.read().root_page;
if root_page == 0 {
let page_id = self.allocate_page();
let node = Node::Leaf {
entries: vec![entry],
};
self.write_node(page_id, node)?;
let mut header = self.inner.header.write();
header.root_page = page_id;
header.entry_count = 1;
header.height = 1;
self.inner.storage.write_header(&header)?;
return Ok(());
}
let mut path = Vec::new();
let leaf_id = self.choose_leaf(root_page, &bbox, &mut path)?;
let split = self.insert_into_leaf(leaf_id, entry)?;
if let Some((new_page, new_bbox)) = split {
self.propagate_split(&path, new_page, new_bbox)?;
} else {
self.update_path_bboxes(&path)?;
}
let mut header = self.inner.header.write();
header.entry_count += 1;
self.inner.storage.write_header(&header)?;
Ok(())
}
fn remove(&self, key: &BoundingBox, nitrite_id: NitriteIdValue) -> SpatialResult<bool> {
self.check_closed()?;
let bbox = InternalBBox::from_bbox(key);
let root_page = self.inner.header.read().root_page;
if root_page == 0 {
return Ok(false);
}
let removed = self.remove_recursive(root_page, &bbox, nitrite_id)?;
if removed {
let mut header = self.inner.header.write();
header.entry_count = header.entry_count.saturating_sub(1);
let root_node = self.read_node(header.root_page)?;
if let Node::Internal { children, .. } = &root_node {
if children.len() == 1 {
let old_root = header.root_page;
header.root_page = children[0].page_id;
header.height = header.height.saturating_sub(1);
self.free_page(old_root);
}
} else if let Node::Leaf { entries } = &root_node {
if entries.is_empty() {
let old_root = header.root_page;
header.root_page = 0;
header.height = 0;
self.free_page(old_root);
}
}
self.inner.storage.write_header(&header)?;
}
Ok(removed)
}
fn find_intersecting_keys(&self, key: &BoundingBox) -> SpatialResult<Vec<NitriteIdValue>> {
self.check_closed()?;
let query = InternalBBox::from_bbox(key);
let root_page = self.inner.header.read().root_page;
if root_page == 0 {
return Ok(Vec::new());
}
let mut results = Vec::new();
self.search_recursive(root_page, &query, &mut results)?;
Ok(results)
}
fn find_contained_keys(&self, key: &BoundingBox) -> SpatialResult<Vec<NitriteIdValue>> {
self.check_closed()?;
let query = InternalBBox::from_bbox(key);
let root_page = self.inner.header.read().root_page;
if root_page == 0 {
return Ok(Vec::new());
}
let mut results = Vec::new();
self.search_contained_recursive(root_page, &query, &mut results)?;
Ok(results)
}
fn size(&self) -> u64 {
self.inner.header.read().entry_count
}
fn close(&self) -> SpatialResult<()> {
let mut closed = self.inner.closed.write();
if *closed {
return Ok(());
}
self.flush()?;
*closed = true;
Ok(())
}
fn clear(&self) -> SpatialResult<()> {
self.check_closed()?;
let dirty_pages = self.inner.cache.write().clear();
for (page_id, node, dirty) in dirty_pages {
if dirty {
let _ = self.inner.storage.write_page(page_id, &node);
}
}
let mut header = self.inner.header.write();
*header = FileHeader::new();
self.inner.storage.write_header(&header)?;
self.inner.storage.sync()?;
Ok(())
}
fn drop_tree(&self) -> SpatialResult<()> {
let mut closed = self.inner.closed.write();
self.inner.cache.write().clear();
self.inner.storage.delete()?;
*closed = true;
Ok(())
}
fn find_nearest(
&self,
center_x: f64,
center_y: f64,
k: usize,
max_distance: Option<f64>,
) -> SpatialResult<Vec<(NitriteIdValue, f64)>> {
Self::find_nearest(self, center_x, center_y, k, max_distance)
}
}
fn compute_entries_bbox(entries: &[LeafEntry]) -> InternalBBox {
let mut bbox = InternalBBox::empty();
for e in entries {
bbox.expand(&e.bbox);
}
bbox
}
fn compute_children_bbox(children: &[ChildRef]) -> InternalBBox {
let mut bbox = InternalBBox::empty();
for c in children {
bbox.expand(&c.bbox);
}
bbox
}
impl Drop for DiskRTree {
fn drop(&mut self) {
if !*self.inner.closed.read() {
let _ = self.flush();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use nitrite::errors::{NitriteError, ErrorKind};
#[test]
fn test_create_empty_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
let tree = DiskRTree::create(&path).unwrap();
assert_eq!(tree.size(), 0);
tree.close().unwrap();
}
#[test]
fn test_nitrite_rtree_api() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 10.0, 10.0), 1).unwrap();
tree.add(&BoundingBox::new(5.0, 5.0, 15.0, 15.0), 2).unwrap();
tree.add(&BoundingBox::new(20.0, 20.0, 30.0, 30.0), 3).unwrap();
assert_eq!(tree.size(), 3);
let results = tree.find_intersecting_keys(&BoundingBox::new(8.0, 8.0, 12.0, 12.0)).unwrap();
assert_eq!(results.len(), 2);
assert!(results.contains(&1));
assert!(results.contains(&2));
let results = tree.find_contained_keys(&BoundingBox::new(-1.0, -1.0, 11.0, 11.0)).unwrap();
assert_eq!(results.len(), 1);
assert!(results.contains(&1));
let removed = tree.remove(&BoundingBox::new(0.0, 0.0, 10.0, 10.0), 1).unwrap();
assert!(removed);
assert_eq!(tree.size(), 2);
tree.clear().unwrap();
assert_eq!(tree.size(), 0);
tree.close().unwrap();
}
#[test]
fn test_persistence_and_lazy_loading() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
{
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 10.0, 10.0), 1).unwrap();
tree.add(&BoundingBox::new(20.0, 20.0, 30.0, 30.0), 2).unwrap();
tree.close().unwrap();
}
{
let tree = DiskRTree::open(&path).unwrap();
let stats = tree.stats();
assert_eq!(stats.cached_pages, 0, "Should not preload any pages");
assert_eq!(stats.total_entries, 2);
let results = tree.find_intersecting_keys(&BoundingBox::new(5.0, 5.0, 15.0, 15.0)).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], 1);
let stats = tree.stats();
assert!(stats.cache_misses > 0, "Should have cache misses from loading");
assert!(stats.disk_reads > 0, "Should have disk reads");
tree.close().unwrap();
}
}
#[test]
fn test_many_inserts_memory_bounded() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
let tree = DiskRTree::create_with_cache_size(&path, 10).unwrap();
for i in 0..1000 {
let x = (i % 100) as f64;
let y = (i / 100) as f64;
tree.add(&BoundingBox::new(x, y, x + 1.0, y + 1.0), i as NitriteIdValue).unwrap();
}
assert_eq!(tree.size(), 1000);
let stats = tree.stats();
assert!(stats.cached_pages <= 10, "Cache should be bounded to 10 pages");
assert!(stats.disk_writes > 0, "Should have written evicted pages to disk");
let results = tree.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 10.0, 2.0)).unwrap();
assert!(!results.is_empty());
tree.close().unwrap();
}
#[test]
fn test_drop_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
{
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 10.0, 10.0), 1).unwrap();
tree.drop_tree().unwrap();
}
let metadata = std::fs::metadata(&path).unwrap();
assert_eq!(metadata.len(), 0);
}
#[test]
fn test_closed_tree_errors() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.close().unwrap();
assert!(tree.add(&BoundingBox::new(0.0, 0.0, 10.0, 10.0), 1).is_err());
assert!(tree.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 10.0, 10.0)).is_err());
}
#[test]
fn test_verify_no_bulk_loading() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.rtree");
{
let tree = DiskRTree::create(&path).unwrap();
for i in 0..500 {
let x = (i % 50) as f64;
let y = (i / 50) as f64;
tree.add(&BoundingBox::new(x, y, x + 1.0, y + 1.0), i as NitriteIdValue).unwrap();
}
tree.close().unwrap();
}
{
let tree = DiskRTree::open(&path).unwrap();
let stats_before = tree.stats();
assert_eq!(stats_before.cached_pages, 0, "No pages should be loaded on open");
assert_eq!(stats_before.disk_reads, 0, "No disk reads should occur on open");
let _ = tree.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 2.0, 2.0)).unwrap();
let stats_after = tree.stats();
assert!(stats_after.cached_pages < 20,
"Should only cache pages along search path, got {}", stats_after.cached_pages);
tree.close().unwrap();
}
}
#[test]
fn test_bulk_load() {
let dir = tempdir().unwrap();
let path = dir.path().join("bulk_load.rtree");
let entries: Vec<_> = (0..100)
.map(|i| {
let x = (i % 10) as f64;
let y = (i / 10) as f64;
(BoundingBox::new(x, y, x + 1.0, y + 1.0), i as NitriteIdValue)
})
.collect();
let tree = DiskRTree::bulk_load(&path, entries).unwrap();
let stats = tree.stats();
assert_eq!(stats.total_entries, 100, "All 100 entries should be in tree");
let results = tree.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 5.0, 5.0)).unwrap();
assert!(!results.is_empty(), "Should find entries in range");
tree.close().unwrap();
}
#[test]
fn test_rebuild() {
let dir = tempdir().unwrap();
let path = dir.path().join("rebuild.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..50 {
let x = (i % 10) as f64;
let y = (i / 10) as f64;
tree.add(&BoundingBox::new(x, y, x + 1.0, y + 1.0), i as NitriteIdValue).unwrap();
}
let stats_before = tree.stats();
assert_eq!(stats_before.total_entries, 50);
let rebuild_stats = tree.rebuild().unwrap();
assert_eq!(rebuild_stats.entries_reindexed, 50);
assert!(rebuild_stats.pages_before > 0);
let results = tree.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 5.0, 5.0)).unwrap();
assert!(!results.is_empty(), "Should find entries after rebuild");
let stats_after = tree.stats();
assert_eq!(stats_after.total_entries, 50);
tree.close().unwrap();
}
#[test]
fn test_spatial_error_to_nitrite_error_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let spatial_err = SpatialError::Io(io_err);
let nitrite_err: NitriteError = spatial_err.into();
assert_eq!(nitrite_err.kind(), &ErrorKind::IOError);
assert!(nitrite_err.message().contains("Spatial I/O error"));
}
#[test]
fn test_spatial_error_to_nitrite_error_serialization() {
let spatial_err = SpatialError::Serialization("bincode failed".to_string());
let nitrite_err: NitriteError = spatial_err.into();
assert_eq!(nitrite_err.kind(), &ErrorKind::EncodingError);
}
#[test]
fn test_spatial_error_to_nitrite_error_invalid_operation() {
let spatial_err = SpatialError::InvalidOperation("cannot split".to_string());
let nitrite_err: NitriteError = spatial_err.into();
assert_eq!(nitrite_err.kind(), &ErrorKind::ValidationError);
}
#[test]
fn test_spatial_error_to_nitrite_error_closed() {
let spatial_err = SpatialError::Closed;
let nitrite_err: NitriteError = spatial_err.into();
assert_eq!(nitrite_err.kind(), &ErrorKind::StoreAlreadyClosed);
assert!(nitrite_err.message().contains("closed"));
}
#[test]
fn test_nitrite_error_to_spatial_error_io() {
let nitrite_err = NitriteError::new("IO error occurred", ErrorKind::IOError);
let spatial_err: SpatialError = nitrite_err.into();
assert!(matches!(spatial_err, SpatialError::Io(_)));
}
#[test]
fn test_nitrite_error_to_spatial_error_encoding() {
let nitrite_err = NitriteError::new("encoding failed", ErrorKind::EncodingError);
let spatial_err: SpatialError = nitrite_err.into();
assert!(matches!(spatial_err, SpatialError::Serialization(_)));
}
#[test]
fn test_nitrite_error_to_spatial_error_closed() {
let nitrite_err = NitriteError::new("store closed", ErrorKind::StoreAlreadyClosed);
let spatial_err: SpatialError = nitrite_err.into();
assert!(matches!(spatial_err, SpatialError::Closed));
}
#[test]
fn test_nitrite_error_to_spatial_error_other() {
let nitrite_err = NitriteError::new("validation failed", ErrorKind::ValidationError);
let spatial_err: SpatialError = nitrite_err.into();
assert!(matches!(spatial_err, SpatialError::InvalidOperation(_)));
}
#[test]
fn test_nitrite_error_to_spatial_error_extension_roundtrip() {
let original = SpatialError::InvalidOperation("test operation".to_string());
let nitrite_err: NitriteError = original.into();
let back: SpatialError = nitrite_err.into();
assert!(matches!(back, SpatialError::InvalidOperation(_)));
}
#[test]
fn test_find_nearest() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_knn.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 0.0, 0.0), 1).unwrap();
let results = tree.find_nearest(0.0, 0.0, 1, None).unwrap();
eprintln!("Results for 1 point, k=1: {:?}", results);
assert_eq!(results.len(), 1, "Expected 1 result");
assert_eq!(results[0].0, 1);
assert_eq!(results[0].1, 0.0);
tree.add(&BoundingBox::new(1.0, 0.0, 1.0, 0.0), 2).unwrap();
let results = tree.find_nearest(0.0, 0.0, 2, None).unwrap();
eprintln!("Results for 2 points, k=2: {:?}", results);
assert_eq!(results.len(), 2, "Expected 2 results, got: {:?}", results);
assert_eq!(results[0].0, 1, "First should be 1, got: {:?}", results);
assert_eq!(results[1].0, 2, "Second should be 2, got: {:?}", results);
tree.close().unwrap();
}
#[test]
fn test_find_nearest_with_bboxes() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_knn_bbox.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 1.0, 1.0), 1).unwrap();
tree.add(&BoundingBox::new(10.0, 10.0, 11.0, 11.0), 2).unwrap();
tree.add(&BoundingBox::new(100.0, 100.0, 101.0, 101.0), 3).unwrap();
eprintln!("Tree size: {}", tree.size());
eprintln!("Tree stats: {:?}", tree.stats());
let all_results = tree.find_nearest(0.5, 0.5, 3, None).unwrap();
eprintln!("All 3 results: {:?}", all_results);
let results = tree.find_nearest(0.5, 0.5, 1, None).unwrap();
eprintln!("Results for bboxes, k=1: {:?}", results);
assert_eq!(results.len(), 1, "Expected 1 result");
assert_eq!(results[0].0, 1, "Should find bbox1 which contains the query point, got id={}", results[0].0);
assert_eq!(results[0].1, 0.0, "Distance should be 0 since point is inside bbox, got {}", results[0].1);
tree.close().unwrap();
}
#[test]
fn test_detect_fragmentation_empty_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_frag.rtree");
let tree = DiskRTree::create(&path).unwrap();
let metrics = tree.detect_fragmentation().unwrap();
assert_eq!(metrics.severity, "None");
assert!(!metrics.should_rebuild);
assert_eq!(metrics.active_pages, 0);
tree.close().unwrap();
}
#[test]
fn test_detect_fragmentation_small_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_frag_small.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..10 {
let x = (i as f64) * 5.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as NitriteIdValue).unwrap();
}
let metrics = tree.detect_fragmentation().unwrap();
assert!(metrics.wasted_space_percent >= 0.0);
assert!(metrics.wasted_space_percent <= 100.0);
assert!(metrics.cache_miss_ratio >= 0.0);
assert!(metrics.cache_miss_ratio <= 1.0);
assert!(metrics.tree_balance_ratio > 0.0);
assert_eq!(metrics.severity, "None");
assert!(!metrics.should_rebuild);
tree.close().unwrap();
}
#[test]
fn test_detect_fragmentation_after_many_operations() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_frag_ops.rtree");
let tree = DiskRTree::create_with_cache_size(&path, 32).unwrap();
for i in 0..200 {
let x = (i % 50) as f64 * 2.0;
let y = (i / 50) as f64 * 2.0;
tree.add(&BoundingBox::new(x, y, x + 1.0, y + 1.0), i as NitriteIdValue).unwrap();
}
for i in (0..200).step_by(3) {
let x = (i % 50) as f64 * 2.0;
let y = (i / 50) as f64 * 2.0;
let _ = tree.remove(&BoundingBox::new(x, y, x + 1.0, y + 1.0), i as NitriteIdValue);
}
let metrics = tree.detect_fragmentation().unwrap();
assert!(metrics.active_pages > 0);
assert!(!metrics.severity.is_empty());
tree.close().unwrap();
}
#[test]
fn test_rebuild_if_fragmented_no_rebuild_needed() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_rebuild_check.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..5 {
tree.add(&BoundingBox::new(i as f64, 0.0, (i + 1) as f64, 1.0), i as NitriteIdValue).unwrap();
}
let (metrics, rebuild_stats) = tree.rebuild_if_fragmented().unwrap();
assert_eq!(metrics.severity, "None");
assert!(!metrics.should_rebuild);
assert!(rebuild_stats.is_none());
tree.close().unwrap();
}
#[test]
fn test_fragmentation_metrics_calculations() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_frag_calc.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..100 {
let x = (i as f64) * 10.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as NitriteIdValue).unwrap();
}
let metrics = tree.detect_fragmentation().unwrap();
assert_eq!(metrics.active_pages, tree.stats().cached_pages);
assert_eq!(metrics.disk_operations,
tree.stats().disk_reads.saturating_add(tree.stats().disk_writes));
let metrics2 = tree.detect_fragmentation().unwrap();
assert_eq!(metrics.wasted_space_percent, metrics2.wasted_space_percent);
assert_eq!(metrics.severity, metrics2.severity);
assert_eq!(metrics.should_rebuild, metrics2.should_rebuild);
tree.close().unwrap();
}
#[test]
fn test_checksum_verification_on_read() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_checksum.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 1.0, 1.0), 1).unwrap();
tree.add(&BoundingBox::new(5.0, 5.0, 6.0, 6.0), 2).unwrap();
tree.close().unwrap();
let tree2 = DiskRTree::open(&path).unwrap();
let results = tree2.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 2.0, 2.0)).unwrap();
assert_eq!(results.len(), 1);
tree2.close().unwrap();
}
#[test]
fn test_integrity_check_empty_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_empty.rtree");
let tree = DiskRTree::create(&path).unwrap();
let report = tree.check_integrity().unwrap();
assert!(report.is_valid);
assert_eq!(report.corrupted_pages.len(), 0);
assert_eq!(report.orphaned_pages.len(), 0);
assert!(report.errors.is_empty());
tree.close().unwrap();
}
#[test]
fn test_integrity_check_with_data() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_data.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..50 {
let x = (i as f64) * 2.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as NitriteIdValue).unwrap();
}
let report = tree.check_integrity().unwrap();
assert!(report.is_valid, "Tree integrity check failed: {:?}", report.errors);
assert_eq!(report.corrupted_pages.len(), 0);
assert!(report.errors.is_empty(), "No integrity errors should be found");
tree.close().unwrap();
}
#[test]
fn test_repair_options_default() {
let opts = RepairOptions::default();
assert!(opts.remove_corrupt);
assert!(opts.rebuild_if_needed);
assert_eq!(opts.max_repairs, None);
}
#[test]
fn test_repair_on_valid_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_repair_valid.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 1.0, 1.0), 1).unwrap();
let report = tree.repair(RepairOptions::default()).unwrap();
assert_eq!(report.pages_removed, 0);
assert!(!report.rebuild_performed);
tree.close().unwrap();
}
#[test]
fn test_migration_check_and_migrate() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_migrate.rtree");
let tree = DiskRTree::create(&path).unwrap();
{
let header = tree.inner.header.read();
assert_eq!(header.version, 1);
}
tree.check_and_migrate().unwrap();
{
let header = tree.inner.header.read();
assert!(header.version >= 1, "Version should be maintained or upgraded");
}
tree.close().unwrap();
}
#[test]
fn test_file_header_with_checksums() {
let header = FileHeader::new();
assert!(header.checksum_enabled);
assert_eq!(header.free_page_count, 0);
assert_eq!(header.free_list_head, 0);
assert_eq!(header.version, 1);
}
#[test]
fn test_persistence_across_close_reopen() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_persistence.rtree");
{
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(10.0, 20.0, 15.0, 25.0), 42).unwrap();
tree.flush().unwrap();
tree.close().unwrap();
}
{
let tree = DiskRTree::open(&path).unwrap();
let results = tree.find_intersecting_keys(&BoundingBox::new(10.0, 20.0, 15.0, 25.0)).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], 42);
tree.close().unwrap();
}
}
#[test]
fn test_integrity_report_accumulation() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_report.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..100 {
let x = (i as f64) * 3.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as NitriteIdValue).unwrap();
}
let report = tree.check_integrity().unwrap();
assert!(report.is_valid, "All pages should be valid");
assert_eq!(report.corrupted_pages.len(), 0);
assert!(report.errors.is_empty(), "No errors should be reported for valid tree");
tree.close().unwrap();
}
#[test]
fn test_migration_manager_version_progression() {
let current = MigrationManager::current_version();
assert!(current > 1, "Should have multiple versions for migration");
let mut old_header = FileHeader::new();
old_header.version = 1;
assert!(MigrationManager::needs_migration(&old_header));
let mut current_header = FileHeader::new();
current_header.version = current;
assert!(!MigrationManager::needs_migration(¤t_header));
}
#[test]
fn test_integrity_empty_tree_positive() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_empty.rtree");
let tree = DiskRTree::create(&path).unwrap();
let report = tree.check_integrity().unwrap();
assert!(report.is_valid);
assert_eq!(report.corrupted_pages.len(), 0);
assert_eq!(report.orphaned_pages.len(), 0);
assert!(report.errors.is_empty());
tree.close().unwrap();
}
#[test]
fn test_integrity_populated_tree_positive() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_pop.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..30 {
let x = (i as f64) * 1.5;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as u64).unwrap();
}
let report = tree.check_integrity().unwrap();
assert!(report.is_valid, "Tree should be valid after additions");
assert_eq!(report.corrupted_pages.len(), 0);
tree.close().unwrap();
}
#[test]
fn test_repair_valid_tree_positive() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_repair_pos.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 10.0, 10.0), 1).unwrap();
let opts = RepairOptions::default();
let report = tree.repair(opts).unwrap();
assert_eq!(report.pages_repaired, 0, "No repairs needed on valid tree");
assert_eq!(report.pages_removed, 0);
assert!(!report.rebuild_performed);
tree.close().unwrap();
}
#[test]
fn test_multiple_integrity_checks_consistent() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_multi_integrity.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..20 {
let x = (i as f64) * 2.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as u64).unwrap();
}
let report1 = tree.check_integrity().unwrap();
let report2 = tree.check_integrity().unwrap();
let report3 = tree.check_integrity().unwrap();
assert_eq!(report1.is_valid, report2.is_valid);
assert_eq!(report2.is_valid, report3.is_valid);
assert_eq!(report1.corrupted_pages.len(), report2.corrupted_pages.len());
tree.close().unwrap();
}
#[test]
fn test_checksum_persistence_across_reopen() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_checksum_persist.rtree");
{
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(5.0, 5.0, 15.0, 15.0), 42).unwrap();
tree.flush().unwrap();
tree.close().unwrap();
}
{
let tree = DiskRTree::open(&path).unwrap();
let results = tree.find_intersecting_keys(&BoundingBox::new(5.0, 5.0, 15.0, 15.0)).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], 42);
tree.close().unwrap();
}
}
#[test]
fn test_migration_preserves_data() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_migrate_data.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 5.0, 5.0), 100).unwrap();
tree.add(&BoundingBox::new(10.0, 10.0, 15.0, 15.0), 200).unwrap();
tree.check_and_migrate().unwrap();
let results = tree.find_intersecting_keys(&BoundingBox::new(0.0, 0.0, 5.0, 5.0)).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], 100);
tree.close().unwrap();
}
#[test]
fn test_integrity_many_pages() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_many.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..100 {
let x = (i as f64) * 2.5;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as u64).unwrap();
}
let report = tree.check_integrity().unwrap();
assert!(report.is_valid);
assert_eq!(report.corrupted_pages.len(), 0);
tree.close().unwrap();
}
#[test]
fn test_repair_with_max_repairs_limit() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_repair_limit.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 1.0, 1.0), 1).unwrap();
let opts = RepairOptions {
remove_corrupt: true,
rebuild_if_needed: false,
max_repairs: Some(5),
};
let report = tree.repair(opts).unwrap();
assert!(report.pages_removed <= 5);
tree.close().unwrap();
}
#[test]
fn test_sequential_migrations() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_seq_migrate.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.check_and_migrate().unwrap();
tree.check_and_migrate().unwrap();
tree.check_and_migrate().unwrap();
let header = tree.inner.header.read();
assert_eq!(header.version, MigrationManager::current_version());
tree.close().unwrap();
}
#[test]
fn test_integrity_after_add() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_integrity_after.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 1.0, 1.0), 1).unwrap();
let report = tree.check_integrity().unwrap();
assert!(report.is_valid);
tree.close().unwrap();
}
#[test]
fn test_repair_after_batch_insert() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_repair_batch.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..50 {
let x = (i as f64) * 3.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as u64).unwrap();
}
let opts = RepairOptions::default();
let report = tree.repair(opts).unwrap();
assert_eq!(report.pages_removed, 0);
tree.close().unwrap();
}
#[test]
fn test_header_validity_after_integrity_check() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_header_valid.rtree");
let tree = DiskRTree::create(&path).unwrap();
let report = tree.check_integrity().unwrap();
assert!(report.is_valid);
let header = tree.inner.header.read();
assert!(header.validate().is_ok());
tree.close().unwrap();
}
#[test]
fn test_repair_with_valid_data_no_rebuild() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_repair_norebuild.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 5.0, 5.0), 1).unwrap();
let opts = RepairOptions {
remove_corrupt: true,
rebuild_if_needed: false,
max_repairs: None,
};
let report = tree.repair(opts).unwrap();
assert!(!report.rebuild_performed);
assert_eq!(report.pages_removed, 0);
tree.close().unwrap();
}
#[test]
fn test_integrity_no_orphans_valid_tree() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_no_orphans.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..20 {
tree.add(&BoundingBox::new(i as f64, 0.0, i as f64 + 1.0, 1.0), i as u64).unwrap();
}
let report = tree.check_integrity().unwrap();
assert_eq!(report.orphaned_pages.len(), 0);
tree.close().unwrap();
}
#[test]
fn test_migration_no_change_at_current_version() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_migrate_none.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.check_and_migrate().unwrap();
let current_version = {
let header = tree.inner.header.read();
header.version
};
tree.check_and_migrate().unwrap();
let new_version = {
let header = tree.inner.header.read();
header.version
};
assert_eq!(current_version, new_version);
tree.close().unwrap();
}
#[test]
fn test_full_cycle_add_check_repair_verify() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_full_cycle.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..30 {
let x = (i as f64) * 1.5;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as u64).unwrap();
}
let integrity_report = tree.check_integrity().unwrap();
assert!(integrity_report.is_valid);
let repair_report = tree.repair(RepairOptions::default()).unwrap();
assert_eq!(repair_report.pages_removed, 0);
let verify_report = tree.check_integrity().unwrap();
assert!(verify_report.is_valid);
tree.close().unwrap();
}
#[test]
fn test_idempotent_migrations() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_idem_migrate.rtree");
let tree = DiskRTree::create(&path).unwrap();
let v1 = {
let h = tree.inner.header.read();
h.version
};
tree.check_and_migrate().unwrap();
let v2 = {
let h = tree.inner.header.read();
h.version
};
tree.check_and_migrate().unwrap();
let v3 = {
let h = tree.inner.header.read();
h.version
};
assert_eq!(v2, v3);
assert!(v2 >= v1);
tree.close().unwrap();
}
#[test]
fn test_data_integrity_through_checks() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_data_integrity.rtree");
let test_data = vec![
(0.0, 0.0, 1.0, 1.0, 100u64),
(10.0, 10.0, 11.0, 11.0, 200u64),
(20.0, 20.0, 21.0, 21.0, 300u64),
];
{
let tree = DiskRTree::create(&path).unwrap();
for (x1, y1, x2, y2, id) in &test_data {
tree.add(&BoundingBox::new(*x1, *y1, *x2, *y2), *id).unwrap();
}
tree.flush().unwrap();
tree.close().unwrap();
}
{
let tree = DiskRTree::open(&path).unwrap();
for (x1, y1, x2, y2, id) in &test_data {
let bbox = BoundingBox::new(*x1, *y1, *x2, *y2);
let results = tree.find_intersecting_keys(&bbox).unwrap();
assert!(results.contains(id), "ID {} should be found", id);
}
tree.close().unwrap();
}
}
#[test]
fn test_repair_with_zero_max_repairs() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_zero_repairs.rtree");
let tree = DiskRTree::create(&path).unwrap();
tree.add(&BoundingBox::new(0.0, 0.0, 1.0, 1.0), 1).unwrap();
let opts = RepairOptions {
remove_corrupt: true,
rebuild_if_needed: false,
max_repairs: Some(0),
};
let report = tree.repair(opts).unwrap();
assert_eq!(report.pages_removed, 0);
tree.close().unwrap();
}
#[test]
fn test_header_consistency_after_operations() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_header_consistency.rtree");
let tree = DiskRTree::create(&path).unwrap();
{
let header = tree.inner.header.read();
assert!(header.checksum_enabled, "Checksums should be enabled on creation");
}
for i in 0..10 {
tree.add(&BoundingBox::new(i as f64, 0.0, i as f64 + 1.0, 1.0), i as u64).unwrap();
}
tree.check_integrity().unwrap();
tree.check_and_migrate().unwrap();
{
let header = tree.inner.header.read();
assert!(header.checksum_enabled, "Checksums should remain enabled after migration");
}
tree.close().unwrap();
}
#[test]
fn test_stress_large_integrity_check() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_stress_large.rtree");
let tree = DiskRTree::create(&path).unwrap();
for i in 0..200 {
let x = (i as f64) * 2.0;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), i as u64).unwrap();
}
let report = tree.check_integrity().unwrap();
assert!(report.is_valid);
tree.close().unwrap();
}
#[test]
fn test_data_survives_integrity_repair_cycles() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_cycles.rtree");
let ids = vec![1u64, 2, 3, 4, 5];
{
let tree = DiskRTree::create(&path).unwrap();
for id in &ids {
let x = *id as f64;
tree.add(&BoundingBox::new(x, 0.0, x + 1.0, 1.0), *id).unwrap();
}
for _ in 0..3 {
tree.check_integrity().unwrap();
tree.repair(RepairOptions::default()).unwrap();
}
tree.close().unwrap();
}
{
let tree = DiskRTree::open(&path).unwrap();
for id in &ids {
let x = *id as f64;
let results = tree.find_intersecting_keys(&BoundingBox::new(x, 0.0, x + 1.0, 1.0)).unwrap();
assert!(results.contains(id));
}
tree.close().unwrap();
}
}
}