use std::collections::HashMap;
use nodedb_types::Namespace;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use crate::storage::engine::{StorageEngine, WriteOp};
pub(crate) async fn flush_spatial<S>(
storage: &S,
checkpoints: &[(String, String, Vec<u8>)],
doc_to_entry: &HashMap<(String, String), u64>,
next_id: u64,
) -> NodeDbResult<()>
where
S: StorageEngine,
{
let mut ops: Vec<WriteOp> = Vec::new();
let index_keys: Vec<(String, String)> = checkpoints
.iter()
.map(|(c, f, _)| (c.clone(), f.clone()))
.collect();
let keys_bytes = zerompk::to_msgpack_vec(&index_keys)
.map_err(|e| NodeDbError::serialization("msgpack", e))?;
ops.push(WriteOp::Put {
ns: Namespace::Spatial,
key: b"spatial:_collections".to_vec(),
value: keys_bytes,
});
let next_id_bytes =
zerompk::to_msgpack_vec(&next_id).map_err(|e| NodeDbError::serialization("msgpack", e))?;
ops.push(WriteOp::Put {
ns: Namespace::Spatial,
key: b"spatial:_next_id".to_vec(),
value: next_id_bytes,
});
for (collection, field, _rtree_bytes) in checkpoints {
let docmap_key = format!("spatial:{collection}:{field}:docmap");
let pairs: Vec<(String, u64)> = doc_to_entry
.iter()
.filter(|((coll, _doc_id), _)| coll == collection)
.map(|((_coll, doc_id), &entry_id)| (doc_id.clone(), entry_id))
.collect();
let docmap_bytes = zerompk::to_msgpack_vec(&pairs)
.map_err(|e| NodeDbError::serialization("msgpack", e))?;
ops.push(WriteOp::Put {
ns: Namespace::Spatial,
key: docmap_key.into_bytes(),
value: docmap_bytes,
});
}
storage
.batch_write(&ops)
.await
.map_err(NodeDbError::storage)?;
#[cfg(not(target_arch = "wasm32"))]
if let Some(seg) = storage.as_spatial_segment_ext() {
for (collection, field, rtree_bytes) in checkpoints {
let wrapped = crate::storage::checksum::wrap(rtree_bytes);
seg.write_spatial_segment(collection, field, &wrapped)
.await
.map_err(NodeDbError::storage)?;
}
return Ok(());
}
let mut rtree_ops: Vec<WriteOp> = Vec::with_capacity(checkpoints.len());
for (collection, field, rtree_bytes) in checkpoints {
let rtree_key = format!("spatial:{collection}:{field}:rtree");
rtree_ops.push(WriteOp::Put {
ns: Namespace::Spatial,
key: rtree_key.into_bytes(),
value: crate::storage::checksum::wrap(rtree_bytes),
});
}
if !rtree_ops.is_empty() {
storage
.batch_write(&rtree_ops)
.await
.map_err(NodeDbError::storage)?;
}
Ok(())
}
pub(crate) async fn restore_spatial<S>(
storage: &S,
) -> NodeDbResult<(
Vec<(String, String, Vec<u8>)>,
HashMap<(String, String), u64>,
u64,
)>
where
S: StorageEngine,
{
let Some(keys_bytes) = storage
.get(Namespace::Spatial, b"spatial:_collections")
.await?
else {
return Ok((Vec::new(), HashMap::new(), 1));
};
let Ok(index_keys) = zerompk::from_msgpack::<Vec<(String, String)>>(&keys_bytes) else {
tracing::warn!("spatial checkpoint: failed to decode collection list — starting fresh");
return Ok((Vec::new(), HashMap::new(), 1));
};
if index_keys.is_empty() {
return Ok((Vec::new(), HashMap::new(), 1));
}
let next_id = if let Some(bytes) = storage.get(Namespace::Spatial, b"spatial:_next_id").await? {
zerompk::from_msgpack::<u64>(&bytes).unwrap_or(1)
} else {
1
};
let mut checkpoints: Vec<(String, String, Vec<u8>)> = Vec::new();
let mut doc_to_entry: HashMap<(String, String), u64> = HashMap::new();
for (collection, field) in &index_keys {
let rtree_envelope: Option<Vec<u8>> = {
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(seg) = storage.as_spatial_segment_ext() {
match seg.open_spatial_segment(collection, field).await {
Ok(Some(boxed)) => Some(boxed.into_vec()),
Ok(None) => {
None
}
Err(e) => {
tracing::warn!(
collection = %collection,
field = %field,
error = %e,
"spatial segment open failed — falling back to KV blob"
);
None
}
}
} else {
None
}
}
#[cfg(target_arch = "wasm32")]
{
None
}
};
let envelope = if let Some(env) = rtree_envelope {
env
} else {
let rtree_key = format!("spatial:{collection}:{field}:rtree");
match storage.get(Namespace::Spatial, rtree_key.as_bytes()).await {
Ok(Some(env)) => env,
Ok(None) => continue,
Err(_) => continue,
}
};
match crate::storage::checksum::unwrap(&envelope) {
Some(bytes) => {
checkpoints.push((collection.clone(), field.clone(), bytes));
}
None => {
tracing::error!(
collection = %collection,
field = %field,
"spatial R-tree CRC32C mismatch — discarding"
);
let rtree_key = format!("spatial:{collection}:{field}:rtree");
let _ = storage
.delete(Namespace::Spatial, rtree_key.as_bytes())
.await;
continue;
}
}
let docmap_key = format!("spatial:{collection}:{field}:docmap");
if let Ok(Some(docmap_bytes)) = storage.get(Namespace::Spatial, docmap_key.as_bytes()).await
&& let Ok(pairs) = zerompk::from_msgpack::<Vec<(String, u64)>>(&docmap_bytes)
{
for (doc_id, entry_id) in pairs {
doc_to_entry.insert((collection.clone(), doc_id), entry_id);
}
}
}
tracing::debug!(
index_count = checkpoints.len(),
doc_entry_count = doc_to_entry.len(),
next_id,
"spatial checkpoint restored"
);
Ok((checkpoints, doc_to_entry, next_id))
}