use nodedb_physical::physical_plan::SpatialOp;
use nodedb_wal::record::RecordType;
use crate::control::server::wal_dispatch::{
encode_spatial_delete_payload, encode_spatial_put_payload,
};
use crate::wal::RedoSubRecord;
pub(super) fn serialize_spatial_op(
op: &SpatialOp,
ops: &mut Vec<RedoSubRecord>,
) -> crate::Result<()> {
match op {
SpatialOp::Insert {
collection,
field,
surrogate,
geometry,
provenance,
} => {
let prov = provenance.as_ref().ok_or_else(|| crate::Error::PlanError {
detail: "spatial insert with no sync provenance has no redo sub-record shape \
and is not supported in transaction resolve"
.to_string(),
})?;
let payload =
encode_spatial_put_payload(collection, field, *surrogate, geometry, prov)?;
let bytes = payload.to_bytes().map_err(crate::Error::Wal)?;
ops.push(RedoSubRecord {
record_type: RecordType::SpatialPut as u32,
payload: bytes,
});
Ok(())
}
SpatialOp::Delete {
collection,
field,
surrogate,
provenance,
} => {
let prov = provenance.as_ref().ok_or_else(|| crate::Error::PlanError {
detail: "spatial delete with no sync provenance has no redo sub-record shape \
and is not supported in transaction resolve"
.to_string(),
})?;
let payload = encode_spatial_delete_payload(collection, field, *surrogate, prov);
let bytes = payload.to_bytes().map_err(crate::Error::Wal)?;
ops.push(RedoSubRecord {
record_type: RecordType::SpatialDelete as u32,
payload: bytes,
});
Ok(())
}
SpatialOp::Scan { .. } => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use nodedb_types::Surrogate;
use nodedb_types::geometry::Geometry;
use nodedb_types::sync::wire::SyncProvenance;
fn prov(seq: u64) -> SyncProvenance {
SyncProvenance {
producer_id: 1,
epoch: 1,
stream_id: 1,
seq,
}
}
fn point(x: f64, y: f64) -> Geometry {
Geometry::point(x, y)
}
#[test]
fn insert_emits_spatial_put_sub_record() {
let op = SpatialOp::Insert {
collection: "places".to_string(),
field: "loc".to_string(),
surrogate: Surrogate::new(7),
geometry: point(10.0, 20.0),
provenance: Some(prov(1)),
};
let mut ops = Vec::new();
serialize_spatial_op(&op, &mut ops).expect("serialize insert");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].record_type, RecordType::SpatialPut as u32);
let decoded = nodedb_wal::record::SpatialPutPayload::from_bytes(&ops[0].payload)
.expect("decode SpatialPutPayload");
assert_eq!(decoded.collection, "places");
assert_eq!(decoded.field, "loc");
let geom: Geometry =
zerompk::from_msgpack(&decoded.geometry_bytes).expect("decode geometry");
assert_eq!(geom, point(10.0, 20.0));
}
#[test]
fn delete_emits_spatial_delete_sub_record() {
let op = SpatialOp::Delete {
collection: "places".to_string(),
field: "loc".to_string(),
surrogate: Surrogate::new(7),
provenance: Some(prov(2)),
};
let mut ops = Vec::new();
serialize_spatial_op(&op, &mut ops).expect("serialize delete");
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].record_type, RecordType::SpatialDelete as u32);
let decoded = nodedb_wal::record::SpatialDeletePayload::from_bytes(&ops[0].payload)
.expect("decode SpatialDeletePayload");
assert_eq!(decoded.collection, "places");
assert_eq!(decoded.field, "loc");
}
#[test]
fn scan_emits_nothing() {
let op = SpatialOp::Scan {
collection: "places".to_string(),
field: "loc".to_string(),
predicate: nodedb_physical::physical_plan::SpatialPredicate::Intersects,
query_geometry: point(0.0, 0.0),
distance_meters: 0.0,
attribute_filters: Vec::new(),
limit: 10,
projection: Vec::new(),
rls_filters: Vec::new(),
prefilter: None,
};
let mut ops = Vec::new();
serialize_spatial_op(&op, &mut ops).expect("serialize scan");
assert!(ops.is_empty(), "read-only scan emits no sub-record");
}
#[test]
fn insert_without_provenance_errors_rather_than_dropping() {
let op = SpatialOp::Insert {
collection: "places".to_string(),
field: "loc".to_string(),
surrogate: Surrogate::new(1),
geometry: point(1.0, 1.0),
provenance: None,
};
let mut ops = Vec::new();
let err = serialize_spatial_op(&op, &mut ops);
assert!(
err.is_err(),
"a spatial insert with no provenance must error, not silently drop"
);
assert!(ops.is_empty());
}
#[test]
fn delete_without_provenance_errors_rather_than_dropping() {
let op = SpatialOp::Delete {
collection: "places".to_string(),
field: "loc".to_string(),
surrogate: Surrogate::new(1),
provenance: None,
};
let mut ops = Vec::new();
let err = serialize_spatial_op(&op, &mut ops);
assert!(
err.is_err(),
"a spatial delete with no provenance must error, not silently drop"
);
assert!(ops.is_empty());
}
}