#![cfg(feature = "sync")]
pub mod common;
use std::sync::Arc;
use std::time::Duration;
use nodedb_lite::sync::{SyncClient, SyncConfig, run_sync_loop};
use nodedb_lite::{NodeDbLite, PagedbStorageMem};
use nodedb_types::geometry::Geometry;
use common::origin::OriginServer;
use common::sql::OriginPgwire;
const COLLECTION: &str = "spatial_sync_test";
const CREATE_ORIGIN: &str =
"CREATE COLLECTION spatial_sync_test WITH (engine='document_schemaless')";
const FIELD: &str = "location";
async fn open_lite() -> Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory()
.await
.expect("open_in_memory");
NodeDbLite::open(storage).await.expect("NodeDbLite::open")
}
async fn start_sync(lite: Arc<NodeDbLite<PagedbStorageMem>>) -> Arc<SyncClient> {
let sync_config = SyncConfig::new(common::origin::ORIGIN_WS, "");
let sync_client = Arc::new(SyncClient::new(sync_config));
let delegate = Arc::clone(&lite) as Arc<dyn nodedb_lite::sync::SyncDelegate>;
let client_clone = Arc::clone(&sync_client);
tokio::spawn(async move {
run_sync_loop(client_clone, delegate).await;
});
let deadline = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => panic!("sync connection did not establish within 10 seconds"),
_ = tokio::time::sleep(Duration::from_millis(50)) => {
if sync_client.state().await == nodedb_lite::sync::SyncState::Connected {
break;
}
}
}
}
sync_client
}
async fn query_dwithin(pg: &OriginPgwire, lng: f64, lat: f64, distance_m: f64) -> usize {
let sql = format!(
"SELECT id FROM {COLLECTION} WHERE st_dwithin(location, \
'{{\"type\":\"Point\",\"coordinates\":[{lng},{lat}]}}', {distance_m}) LIMIT 100"
);
pg.query(&sql).await.len()
}
#[tokio::test]
async fn spatial_inserts_replicate_to_origin() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
pg.execute(CREATE_ORIGIN).await;
let lite = open_lite().await;
let _sync = start_sync(Arc::clone(&lite)).await;
let points: &[(&str, f64, f64)] = &[
("london_a", -0.10, 51.50),
("london_b", -0.15, 51.52),
("london_c", -0.08, 51.48),
];
for (id, lng, lat) in points {
let geom = Geometry::point(*lng, *lat);
lite.spatial_insert(COLLECTION, FIELD, id, &geom);
}
let mut origin_count: usize = 0;
let deadline = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => break,
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let count = query_dwithin(&pg, -0.11, 51.50, 50_000.0).await;
if count >= 3 {
origin_count = count;
break;
}
}
}
}
assert_eq!(
origin_count, 3,
"Origin st_dwithin must return 3 results after sync; got {origin_count}"
);
pg.execute(&format!("DROP COLLECTION {COLLECTION}")).await;
}
#[tokio::test]
async fn spatial_delete_replicates_to_origin() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
pg.execute(CREATE_ORIGIN).await;
let lite = open_lite().await;
let _sync = start_sync(Arc::clone(&lite)).await;
let bg_points: &[(&str, f64, f64)] = &[("bg_a", -0.20, 51.55), ("bg_b", -0.05, 51.45)];
for (id, lng, lat) in bg_points {
let geom = Geometry::point(*lng, *lat);
lite.spatial_insert(COLLECTION, FIELD, id, &geom);
}
let target_geom = Geometry::point(-0.13, 51.51);
lite.spatial_insert(COLLECTION, FIELD, "target_pt", &target_geom);
let deadline = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(deadline);
let mut all_appeared = false;
loop {
tokio::select! {
_ = &mut deadline => break,
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let count = query_dwithin(&pg, -0.12, 51.50, 50_000.0).await;
if count >= 3 {
all_appeared = true;
break;
}
}
}
}
assert!(
all_appeared,
"all 3 points must appear on Origin before testing delete"
);
lite.spatial_delete(COLLECTION, FIELD, "target_pt");
let deadline = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(deadline);
let mut target_gone = false;
loop {
tokio::select! {
_ = &mut deadline => break,
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let count = query_dwithin(&pg, -0.13, 51.51, 100.0).await;
if count == 0 {
target_gone = true;
break;
}
}
}
}
assert!(
target_gone,
"after SpatialDelete replicates, target_pt must return 0 results on Origin"
);
let bg_count = query_dwithin(&pg, -0.12, 51.50, 50_000.0).await;
assert_eq!(
bg_count, 2,
"background points must still be present after target delete; got {bg_count}"
);
pg.execute(&format!("DROP COLLECTION {COLLECTION}")).await;
}
#[tokio::test]
async fn spatial_pre_connection_inserts_sync_after_connect() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
pg.execute(CREATE_ORIGIN).await;
let lite = open_lite().await;
let pre_points: &[(&str, f64, f64)] = &[
("pre_a", 2.30, 48.85), ("pre_b", 2.35, 48.87),
];
for (id, lng, lat) in pre_points {
let geom = Geometry::point(*lng, *lat);
lite.spatial_insert(COLLECTION, FIELD, id, &geom);
}
let _sync = start_sync(Arc::clone(&lite)).await;
let mut origin_count: usize = 0;
let deadline = tokio::time::sleep(Duration::from_secs(8));
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => break,
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let count = query_dwithin(&pg, 2.32, 48.86, 50_000.0).await;
if count >= 2 {
origin_count = count;
break;
}
}
}
}
assert_eq!(
origin_count, 2,
"pre-connection spatial inserts must sync after connect; got {origin_count}"
);
pg.execute(&format!("DROP COLLECTION {COLLECTION}")).await;
}
#[tokio::test]
async fn spatial_collection_registers_on_origin_via_announce() {
let Some(_origin) = OriginServer::try_spawn_with_pgwire() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let pg = OriginPgwire::connect().await;
let lite = open_lite().await;
lite.create_collection(COLLECTION, &[])
.await
.expect("Lite create_collection spatial_sync_test");
let _sync = start_sync(Arc::clone(&lite)).await;
let points: &[(&str, f64, f64)] = &[
("reg_a", -0.10, 51.50),
("reg_b", -0.15, 51.52),
("reg_c", -0.08, 51.48),
];
for (id, lng, lat) in points {
let geom = Geometry::point(*lng, *lat);
lite.spatial_insert(COLLECTION, FIELD, id, &geom);
}
let mut catalog_visible = false;
let deadline = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => break,
_ = tokio::time::sleep(Duration::from_millis(200)) => {
if let Ok(rows) = pg
.try_query("SELECT relname FROM pg_class WHERE relname = 'spatial_sync_test'")
.await
&& !rows.is_empty()
{
catalog_visible = true;
break;
}
}
}
}
assert!(
catalog_visible,
"spatial_sync_test must register in Origin's pg_class via the spatial-push \
CollectionSchema announce (no Origin pre-create) within the deadline"
);
let mut origin_count: usize = 0;
let deadline = tokio::time::sleep(Duration::from_secs(8));
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => break,
_ = tokio::time::sleep(Duration::from_millis(200)) => {
let count = query_dwithin(&pg, -0.11, 51.50, 50_000.0).await;
if count >= 3 {
origin_count = count;
break;
}
}
}
}
assert_eq!(
origin_count, 3,
"Origin st_dwithin must return 3 results after registration via the \
spatial-push announce (no Origin pre-create); got {origin_count}"
);
pg.execute(&format!("DROP COLLECTION {COLLECTION}")).await;
}