use super::{PhysicalPlan, QueryOp};
#[derive(Debug, thiserror::Error)]
pub enum WireError {
#[error("{0}")]
InvalidPlan(&'static str),
#[error("plan codec: {0}")]
Codec(String),
}
pub fn encode(plan: &PhysicalPlan) -> Result<Vec<u8>, WireError> {
if matches!(plan, PhysicalPlan::ClusterArray(_)) {
return Err(WireError::InvalidPlan(
"ClusterArray plans must not be sent over the wire",
));
}
if matches!(
plan,
PhysicalPlan::Query(QueryOp::ShuffleJoinConsume { .. })
) {
return Err(WireError::InvalidPlan(
"ShuffleJoinConsume plans carry node-local paths and must not be sent over the wire",
));
}
if matches!(
plan,
PhysicalPlan::Query(QueryOp::ShuffleAggregateConsume { .. })
) {
return Err(WireError::InvalidPlan(
"ShuffleAggregateConsume plans carry node-local paths and must not be sent over the wire",
));
}
zerompk::to_msgpack_vec(plan).map_err(|e| WireError::Codec(format!("encode: {e}")))
}
pub fn decode(bytes: &[u8]) -> Result<PhysicalPlan, WireError> {
zerompk::from_msgpack(bytes).map_err(|e| WireError::Codec(format!("decode: {e}")))
}
pub fn encode_batch(plans: &Vec<PhysicalPlan>) -> Result<Vec<u8>, WireError> {
for plan in plans {
if matches!(plan, PhysicalPlan::ClusterArray(_)) {
return Err(WireError::InvalidPlan(
"ClusterArray plans must not be shipped via the sequencer",
));
}
if matches!(
plan,
PhysicalPlan::Query(QueryOp::ShuffleJoinConsume { .. })
) {
return Err(WireError::InvalidPlan(
"ShuffleJoinConsume plans carry node-local paths and must not be shipped via the sequencer",
));
}
if matches!(
plan,
PhysicalPlan::Query(QueryOp::ShuffleAggregateConsume { .. })
) {
return Err(WireError::InvalidPlan(
"ShuffleAggregateConsume plans carry node-local paths and must not be shipped via the sequencer",
));
}
}
zerompk::to_msgpack_vec(plans).map_err(|e| WireError::Codec(format!("batch encode: {e}")))
}
pub fn decode_batch(bytes: &[u8]) -> Result<Vec<PhysicalPlan>, WireError> {
zerompk::from_msgpack(bytes).map_err(|e| WireError::Codec(format!("batch decode: {e}")))
}
#[cfg(test)]
mod tests {
use nodedb_types::{DatabaseId, QualifiedCollection};
use super::*;
use crate::physical_plan::JoinProjection;
#[test]
fn hash_join_tail_semantics_roundtrip() {
let plan = PhysicalPlan::Query(QueryOp::HashJoin {
left_collection: QualifiedCollection::new(DatabaseId::DEFAULT, "left"),
right_collection: QualifiedCollection::new(DatabaseId::DEFAULT, "right"),
left_alias: Some("l".into()),
right_alias: Some("r".into()),
on: vec![("id".into(), "id".into())],
join_type: "left".into(),
limit: 10,
projection: vec![JoinProjection {
source: "l.id".into(),
output: "id".into(),
}],
computed_projection: vec![1, 2],
join_filters: vec![3, 4],
post_filters: vec![5, 6],
left_input: None,
right_input: None,
left_rls_filters: Vec::new(),
right_rls_filters: Vec::new(),
left_scan_filters: Vec::new(),
right_scan_filters: Vec::new(),
left_bitmap: None,
right_bitmap: None,
});
let encoded = encode(&plan).expect("hash join encodes");
let decoded = decode(&encoded).expect("hash join decodes");
assert_eq!(decoded, plan);
}
}