Skip to main content

nodedb_lite/query/spatial_ops/
writes.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Spatial write operations: Insert and Delete.
3
4use nodedb_types::Surrogate;
5use nodedb_types::geometry::Geometry;
6use nodedb_types::result::QueryResult;
7
8use crate::error::LiteError;
9use crate::query::engine::LiteQueryEngine;
10use crate::storage::engine::StorageEngine;
11
12/// `SpatialOp::Insert` — index a geometry for a document surrogate.
13///
14/// The surrogate is used as the stable doc ID string, matching the
15/// hex-encoded key that Origin uses so that cross-engine prefilter
16/// bitmap intersects work without translation.
17pub fn spatial_insert<S: StorageEngine>(
18    engine: &LiteQueryEngine<S>,
19    collection: &str,
20    field: &str,
21    surrogate: Surrogate,
22    geometry: &Geometry,
23) -> Result<QueryResult, LiteError> {
24    let doc_id = surrogate.0.to_string();
25    engine
26        .spatial
27        .lock()
28        .map_err(|_| LiteError::LockPoisoned)?
29        .index_document(collection, field, &doc_id, geometry);
30    // Stage for durable sync outbound (sync method — no await needed).
31    #[cfg(not(target_arch = "wasm32"))]
32    if let Some(q) = &engine.spatial_outbound {
33        q.stage_insert(collection, field, &doc_id, geometry);
34    }
35    Ok(QueryResult {
36        columns: vec![],
37        rows: vec![],
38        rows_affected: 1,
39        command: None,
40    })
41}
42
43/// `SpatialOp::Delete` — remove a document's geometry from the R-tree index.
44pub fn spatial_delete<S: StorageEngine>(
45    engine: &LiteQueryEngine<S>,
46    collection: &str,
47    field: &str,
48    surrogate: Option<Surrogate>,
49) -> Result<QueryResult, LiteError> {
50    // No surrogate means no row to remove.
51    let Some(surrogate) = surrogate else {
52        return Ok(QueryResult {
53            columns: vec![],
54            rows: vec![],
55            rows_affected: 0,
56            command: None,
57        });
58    };
59    let doc_id = surrogate.0.to_string();
60    engine
61        .spatial
62        .lock()
63        .map_err(|_| LiteError::LockPoisoned)?
64        .remove_document(collection, field, &doc_id);
65    // Stage for durable sync outbound.
66    #[cfg(not(target_arch = "wasm32"))]
67    if let Some(q) = &engine.spatial_outbound {
68        q.stage_delete(collection, field, &doc_id);
69    }
70    Ok(QueryResult {
71        columns: vec![],
72        rows: vec![],
73        rows_affected: 1,
74        command: None,
75    })
76}
77
78#[cfg(test)]
79mod tests {
80    use std::sync::{Arc, Mutex};
81
82    use nodedb_types::BoundingBox;
83    use nodedb_types::Surrogate;
84    use nodedb_types::geometry::Geometry;
85
86    use crate::engine::spatial::SpatialIndexManager;
87
88    fn make_mgr() -> Arc<Mutex<SpatialIndexManager>> {
89        let governor = crate::query::engine::test_governor();
90        Arc::new(Mutex::new(SpatialIndexManager::new(
91            crate::query::engine::test_scoped_memory(&governor, nodedb_mem::EngineId::Spatial),
92        )))
93    }
94
95    #[test]
96    fn insert_indexes_geometry() {
97        let mgr = make_mgr();
98        let doc_id = Surrogate(42).0.to_string();
99        mgr.lock()
100            .unwrap()
101            .index_document("col", "geom", &doc_id, &Geometry::point(10.0, 20.0));
102        let guard = mgr.lock().unwrap();
103        let hits = guard.search("col", "geom", &BoundingBox::new(9.0, 19.0, 11.0, 21.0));
104        assert_eq!(hits.len(), 1);
105    }
106
107    #[test]
108    fn delete_removes_geometry() {
109        let mgr = make_mgr();
110        let doc_id = Surrogate(7).0.to_string();
111        mgr.lock()
112            .unwrap()
113            .index_document("col", "geom", &doc_id, &Geometry::point(10.0, 20.0));
114        mgr.lock().unwrap().remove_document("col", "geom", &doc_id);
115        let guard = mgr.lock().unwrap();
116        let hits = guard.search("col", "geom", &BoundingBox::new(0.0, 0.0, 180.0, 90.0));
117        assert!(hits.is_empty());
118    }
119}