nodedb_physical/physical_plan/
wire.rs1use super::{PhysicalPlan, QueryOp};
9
10#[derive(Debug, thiserror::Error)]
14pub enum WireError {
15 #[error("{0}")]
16 InvalidPlan(&'static str),
17 #[error("plan codec: {0}")]
18 Codec(String),
19}
20
21pub fn encode(plan: &PhysicalPlan) -> Result<Vec<u8>, WireError> {
35 if matches!(plan, PhysicalPlan::ClusterArray(_)) {
36 return Err(WireError::InvalidPlan(
37 "ClusterArray plans must not be sent over the wire",
38 ));
39 }
40 if matches!(
41 plan,
42 PhysicalPlan::Query(QueryOp::ShuffleJoinConsume { .. })
43 ) {
44 return Err(WireError::InvalidPlan(
45 "ShuffleJoinConsume plans carry node-local paths and must not be sent over the wire",
46 ));
47 }
48 if matches!(
49 plan,
50 PhysicalPlan::Query(QueryOp::ShuffleAggregateConsume { .. })
51 ) {
52 return Err(WireError::InvalidPlan(
53 "ShuffleAggregateConsume plans carry node-local paths and must not be sent over the wire",
54 ));
55 }
56 zerompk::to_msgpack_vec(plan).map_err(|e| WireError::Codec(format!("encode: {e}")))
57}
58
59pub fn decode(bytes: &[u8]) -> Result<PhysicalPlan, WireError> {
61 zerompk::from_msgpack(bytes).map_err(|e| WireError::Codec(format!("decode: {e}")))
62}
63
64pub fn encode_batch(plans: &Vec<PhysicalPlan>) -> Result<Vec<u8>, WireError> {
69 for plan in plans {
70 if matches!(plan, PhysicalPlan::ClusterArray(_)) {
71 return Err(WireError::InvalidPlan(
72 "ClusterArray plans must not be shipped via the sequencer",
73 ));
74 }
75 if matches!(
76 plan,
77 PhysicalPlan::Query(QueryOp::ShuffleJoinConsume { .. })
78 ) {
79 return Err(WireError::InvalidPlan(
80 "ShuffleJoinConsume plans carry node-local paths and must not be shipped via the sequencer",
81 ));
82 }
83 if matches!(
84 plan,
85 PhysicalPlan::Query(QueryOp::ShuffleAggregateConsume { .. })
86 ) {
87 return Err(WireError::InvalidPlan(
88 "ShuffleAggregateConsume plans carry node-local paths and must not be shipped via the sequencer",
89 ));
90 }
91 }
92 zerompk::to_msgpack_vec(plans).map_err(|e| WireError::Codec(format!("batch encode: {e}")))
93}
94
95pub fn decode_batch(bytes: &[u8]) -> Result<Vec<PhysicalPlan>, WireError> {
100 zerompk::from_msgpack(bytes).map_err(|e| WireError::Codec(format!("batch decode: {e}")))
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::physical_plan::JoinProjection;
107
108 #[test]
109 fn hash_join_tail_semantics_roundtrip() {
110 let plan = PhysicalPlan::Query(QueryOp::HashJoin {
111 left_collection: "left".into(),
112 right_collection: "right".into(),
113 left_alias: Some("l".into()),
114 right_alias: Some("r".into()),
115 on: vec![("id".into(), "id".into())],
116 join_type: "left".into(),
117 limit: 10,
118 post_group_by: Vec::new(),
119 post_aggregates: Vec::new(),
120 projection: vec![JoinProjection {
121 source: "l.id".into(),
122 output: "id".into(),
123 }],
124 computed_projection: vec![1, 2],
125 join_filters: vec![3, 4],
126 post_filters: vec![5, 6],
127 left_input: None,
128 right_input: None,
129 left_bitmap: None,
130 right_bitmap: None,
131 });
132
133 let encoded = encode(&plan).expect("hash join encodes");
134 let decoded = decode(&encoded).expect("hash join decodes");
135 assert_eq!(decoded, plan);
136 }
137}