use nodedb_types::TenantId;
use crate::control::server::dispatch_utils::dispatch_to_data_plane;
use crate::control::state::SharedState;
use crate::types::{DatabaseId, TraceId, VShardId};
use nodedb_physical::physical_plan::{DocumentOp, PhysicalPlan};
#[derive(Clone)]
pub struct ScannedEdge {
pub surrogate: u32,
pub from: String,
pub to: String,
pub label: Option<String>,
pub weight: Option<f64>,
}
pub struct PreexecScan {
pub surrogates: Vec<u32>,
pub edges: Vec<ScannedEdge>,
}
pub async fn run_preexec_scan(
shared: &SharedState,
tenant_id: TenantId,
database_id: DatabaseId,
collection: &str,
filter_bytes: Vec<u8>,
) -> crate::Result<PreexecScan> {
let vshard_id = VShardId::from_collection_in_database(database_id, collection);
let scan_plan = PhysicalPlan::Document(DocumentOp::Scan {
collection: collection.to_owned(),
filters: filter_bytes,
limit: usize::MAX,
offset: 0,
sort_keys: vec![],
distinct: false,
projection: vec![
"_from".to_string(),
"_to".to_string(),
"_type".to_string(),
"weight".to_string(),
],
computed_columns: vec![],
window_functions: vec![],
system_time: nodedb_types::SystemTimeScope::Current,
valid_at_ms: None,
prefilter: None,
});
if let Some(gateway) = shared.gateway.get() {
let gw_ctx = crate::control::gateway::core::QueryContext {
tenant_id,
trace_id: TraceId::ZERO,
database_id,
txn_id: None,
};
let payloads =
gateway
.execute(&gw_ctx, scan_plan)
.await
.map_err(|e| crate::Error::Storage {
engine: "preexec-scan".into(),
detail: format!("pre-execution scan failed: {e}"),
})?;
let payload = payloads.into_iter().next().unwrap_or_default();
return Ok(decode_scan(&payload));
}
let response = dispatch_to_data_plane(
shared,
tenant_id,
database_id,
vshard_id,
scan_plan,
TraceId::ZERO,
)
.await?;
if response.status != crate::bridge::envelope::Status::Ok {
return Err(crate::Error::Storage {
engine: "preexec-scan".into(),
detail: format!("pre-execution scan failed: {:?}", response.error_code),
});
}
Ok(decode_scan(&response.payload))
}
fn decode_scan(payload: &[u8]) -> PreexecScan {
if payload.is_empty() {
return PreexecScan {
surrogates: vec![],
edges: vec![],
};
}
let json_str = nodedb_types::msgpack_to_json_string(payload)
.unwrap_or_else(|_| String::from_utf8_lossy(payload).into_owned());
decode_scan_json(&json_str)
}
fn decode_scan_json(json_str: &str) -> PreexecScan {
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
let mut surrogates = Vec::new();
let mut edges = Vec::new();
if let Ok(rows) = sonic_rs::from_str::<sonic_rs::Value>(json_str)
&& rows.is_array()
{
for row in rows.as_array().into_iter().flatten() {
let surrogate = row
.get("id")
.and_then(|id_val| id_val.as_str())
.filter(|id_str| id_str.len() == 8)
.and_then(|id_str| u32::from_str_radix(id_str, 16).ok());
if let Some(surrogate) = surrogate {
surrogates.push(surrogate);
}
let data = row.get("data");
let from = data
.as_ref()
.and_then(|d| d.get("_from"))
.and_then(|v| v.as_str());
let to = data
.as_ref()
.and_then(|d| d.get("_to"))
.and_then(|v| v.as_str());
if let (Some(surrogate), Some(from), Some(to)) = (surrogate, from, to) {
let label = data
.as_ref()
.and_then(|d| d.get("_type"))
.and_then(|v| v.as_str())
.map(str::to_string);
let weight = data
.as_ref()
.and_then(|d| d.get("weight"))
.and_then(|v| v.as_f64())
.filter(|w| w.is_finite());
edges.push(ScannedEdge {
surrogate,
from: from.to_string(),
to: to.to_string(),
label,
weight,
});
}
}
}
surrogates.sort_unstable();
PreexecScan { surrogates, edges }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_empty_payload_returns_empty() {
let scan = decode_scan(&[]);
assert!(scan.surrogates.is_empty());
assert!(scan.edges.is_empty());
}
#[test]
fn decode_json_extracts_surrogates_and_edges() {
let json = r#"[
{"id":"0000002a","data":{"_from":"a","_to":"b","_type":"ROAD","weight":5.0}},
{"id":"0000000b","data":{"_from":"c","_to":"d"}},
{"id":"00000001","data":{"name":"alice"}}
]"#;
let scan = decode_scan_json(json);
assert_eq!(scan.surrogates, vec![1, 11, 42]);
assert_eq!(scan.edges.len(), 2);
let road = scan
.edges
.iter()
.find(|e| e.from == "a")
.expect("edge a->b present");
assert_eq!(road.to, "b");
assert_eq!(road.label.as_deref(), Some("ROAD"));
assert_eq!(road.surrogate, 42);
assert_eq!(road.weight, Some(5.0));
let untyped = scan
.edges
.iter()
.find(|e| e.from == "c")
.expect("edge c->d present");
assert_eq!(untyped.to, "d");
assert_eq!(untyped.label, None);
assert_eq!(untyped.surrogate, 11);
assert_eq!(untyped.weight, None);
}
#[test]
fn decode_json_row_without_both_endpoints_is_not_an_edge() {
let json = r#"[{"id":"00000005","data":{"_from":"x"}}]"#;
let scan = decode_scan_json(json);
assert_eq!(scan.surrogates, vec![5]);
assert!(scan.edges.is_empty());
}
}