use nodedb_lite::storage::engine::StorageEngine;
use nodedb_lite::{Encryption, NodeDbLite, PagedbStorageDefault, PagedbStorageMem};
use nodedb_spatial::predicates::{contains::st_contains, intersects::st_intersects};
use nodedb_types::BoundingBox;
use nodedb_types::geometry::Geometry;
const COLLECTION: &str = "places";
const FIELD: &str = "location";
fn sample_points() -> Vec<(&'static str, Geometry)> {
vec![
("london", Geometry::point(-0.1278, 51.5074)),
("new_york", Geometry::point(-74.0060, 40.7128)),
("tokyo", Geometry::point(139.6917, 35.6895)),
("sydney", Geometry::point(151.2093, -33.8688)),
("nairobi", Geometry::point(36.8219, -1.2921)),
]
}
fn europe_bbox() -> BoundingBox {
BoundingBox::new(-30.0, 30.0, 40.0, 72.0)
}
fn eurasia_bbox() -> BoundingBox {
BoundingBox::new(-30.0, 0.0, 180.0, 72.0)
}
fn western_europe_polygon() -> Geometry {
Geometry::polygon(vec![vec![
[-25.0, 35.0],
[30.0, 35.0],
[30.0, 65.0],
[-25.0, 65.0],
[-25.0, 35.0], ]])
}
async fn open_in_memory() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory()
.await
.expect("open in-memory storage");
NodeDbLite::open(storage).await.expect("open NodeDbLite")
}
#[tokio::test]
async fn bbox_query_returns_expected_subset() {
let db = open_in_memory().await;
for (id, geom) in sample_points() {
db.spatial_insert(COLLECTION, FIELD, id, &geom);
}
let europe_results = db.spatial_search_bbox(COLLECTION, FIELD, &europe_bbox());
assert_eq!(
europe_results.len(),
1,
"expected exactly 1 hit in Europe bbox, got {}: {:?}",
europe_results.len(),
europe_results,
);
let eurasia_results = db.spatial_search_bbox(COLLECTION, FIELD, &eurasia_bbox());
assert_eq!(
eurasia_results.len(),
2,
"expected 2 hits in Eurasia bbox, got {}: {:?}",
eurasia_results.len(),
eurasia_results,
);
let global_bbox = BoundingBox::new(-180.0, -90.0, 180.0, 90.0);
let global_results = db.spatial_search_bbox(COLLECTION, FIELD, &global_bbox);
assert_eq!(
global_results.len(),
5,
"expected all 5 points in global bbox, got {}",
global_results.len(),
);
}
#[tokio::test]
async fn ogc_predicates_point_in_polygon() {
let _db = open_in_memory().await;
let poly = western_europe_polygon();
let london = Geometry::point(-0.1278, 51.5074);
assert!(
st_contains(&poly, &london),
"expected London to be inside western_europe_polygon"
);
let tokyo = Geometry::point(139.6917, 35.6895);
assert!(
!st_contains(&poly, &tokyo),
"expected Tokyo to be outside western_europe_polygon"
);
}
#[tokio::test]
async fn ogc_predicates_intersects() {
let _db = open_in_memory().await;
let poly_a = western_europe_polygon();
let poly_b = Geometry::polygon(vec![vec![
[10.0, 35.0],
[50.0, 35.0],
[50.0, 65.0],
[10.0, 65.0],
[10.0, 35.0],
]]);
assert!(
st_intersects(&poly_a, &poly_b),
"overlapping polygons should intersect"
);
let southern = Geometry::polygon(vec![vec![
[-20.0, -60.0],
[20.0, -60.0],
[20.0, -20.0],
[-20.0, -20.0],
[-20.0, -60.0],
]]);
assert!(
!st_intersects(&poly_a, &southern),
"non-overlapping polygons should not intersect"
);
}
#[tokio::test]
async fn ogc_predicates_contains() {
let _db = open_in_memory().await;
let outer = western_europe_polygon();
let inner = Geometry::polygon(vec![vec![
[-5.0, 40.0],
[10.0, 40.0],
[10.0, 55.0],
[-5.0, 55.0],
[-5.0, 40.0],
]]);
assert!(
st_contains(&outer, &inner),
"outer polygon should contain inner polygon"
);
let straddling = Geometry::polygon(vec![vec![
[25.0, 35.0],
[45.0, 35.0],
[45.0, 55.0],
[25.0, 55.0],
[25.0, 35.0],
]]);
assert!(
!st_contains(&outer, &straddling),
"straddling polygon should not be fully contained"
);
}
#[tokio::test]
async fn spatial_index_persists_across_restart() {
let dir = tempfile::tempdir().expect("create tempdir");
let path = dir.path().join("spatial_test.db");
let pre_restart_ids: Vec<u64>;
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let db = NodeDbLite::open(storage).await.expect("open NodeDbLite");
for (id, geom) in sample_points() {
db.spatial_insert(COLLECTION, FIELD, id, &geom);
}
let results_before = db.spatial_search_bbox(
COLLECTION,
FIELD,
&BoundingBox::new(-180.0, -90.0, 180.0, 90.0),
);
assert_eq!(results_before.len(), 5, "expected 5 results before flush");
pre_restart_ids = results_before.iter().map(|e| e.id).collect();
db.flush().await.expect("flush");
db.shutdown().await;
}
{
use nodedb_types::Namespace;
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("storage for count check");
let spatial_count = storage
.count(Namespace::Spatial)
.await
.expect("spatial count");
assert!(
spatial_count > 0,
"Namespace::Spatial should have entries after flush, got 0"
);
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen NodeDbLite");
let results_after = db.spatial_search_bbox(
COLLECTION,
FIELD,
&BoundingBox::new(-180.0, -90.0, 180.0, 90.0),
);
assert_eq!(
results_after.len(),
5,
"expected 5 results after restart, got {}",
results_after.len()
);
let mut post_ids: Vec<u64> = results_after.iter().map(|e| e.id).collect();
let mut pre_sorted = pre_restart_ids.clone();
post_ids.sort_unstable();
pre_sorted.sort_unstable();
assert_eq!(
pre_sorted, post_ids,
"entry IDs must be identical after restart"
);
let europe_after = db.spatial_search_bbox(COLLECTION, FIELD, &europe_bbox());
assert_eq!(
europe_after.len(),
1,
"Europe bbox should return 1 result after restart"
);
}
}
#[tokio::test]
async fn upsert_and_delete_after_restart() {
let dir = tempfile::tempdir().expect("create tempdir");
let path = dir.path().join("spatial_upsert.db");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let db = NodeDbLite::open(storage).await.expect("open db");
db.spatial_insert(
COLLECTION,
FIELD,
"london",
&Geometry::point(-0.1278, 51.5074),
);
db.flush().await.expect("flush");
db.shutdown().await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
db.spatial_insert(COLLECTION, FIELD, "london", &Geometry::point(-25.0, -40.0));
let europe = db.spatial_search_bbox(COLLECTION, FIELD, &europe_bbox());
assert!(
europe.is_empty(),
"after upsert, old European position should be gone"
);
let south_atlantic = BoundingBox::new(-30.0, -50.0, -20.0, -30.0);
let south = db.spatial_search_bbox(COLLECTION, FIELD, &south_atlantic);
assert_eq!(
south.len(),
1,
"upserted position should appear in south-atlantic bbox"
);
}
}