Skip to main content

linkrs_query/planning/plan/explain/
description.rs

1use serde::{Deserialize, Serialize};
2
3/// Node description key-value pair
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Pair {
6    pub key: String,
7    pub value: String,
8}
9
10impl Pair {
11    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
12        Self {
13            key: key.into(),
14            value: value.into(),
15        }
16    }
17}
18
19/// Branch information
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PlanNodeBranchInfo {
22    pub is_do_branch: bool,
23    pub condition_node_id: i64,
24}
25
26impl PlanNodeBranchInfo {
27    pub fn new(is_do_branch: bool, condition_node_id: i64) -> Self {
28        Self {
29            is_do_branch,
30            condition_node_id,
31        }
32    }
33}
34
35/// Performance statistics
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ProfilingStats {
38    pub rows: i64,
39    pub exec_duration_in_us: i64,
40    pub total_duration_in_us: i64,
41    pub other_stats: std::collections::HashMap<String, String>,
42}
43
44impl ProfilingStats {
45    pub fn new() -> Self {
46        Self {
47            rows: 0,
48            exec_duration_in_us: 0,
49            total_duration_in_us: 0,
50            other_stats: std::collections::HashMap::new(),
51        }
52    }
53}
54
55impl Default for ProfilingStats {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61/// Plan Node Description
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PlanNodeDescription {
64    pub name: String,
65    pub id: i64,
66    pub output_var: String,
67    pub description: Option<Vec<Pair>>,
68    pub profiles: Option<Vec<ProfilingStats>>,
69    pub branch_info: Option<PlanNodeBranchInfo>,
70    pub dependencies: Option<Vec<i64>>,
71}
72
73impl PlanNodeDescription {
74    pub fn new(name: impl Into<String>, id: i64) -> Self {
75        Self {
76            name: name.into(),
77            id,
78            output_var: String::new(),
79            description: None,
80            profiles: None,
81            branch_info: None,
82            dependencies: None,
83        }
84    }
85
86    pub fn with_output_var(mut self, output_var: impl Into<String>) -> Self {
87        self.output_var = output_var.into();
88        self
89    }
90
91    pub fn add_description(&mut self, key: impl Into<String>, value: impl Into<String>) {
92        if self.description.is_none() {
93            self.description = Some(Vec::new());
94        }
95        self.description
96            .as_mut()
97            .expect("description should be Some after initialization")
98            .push(Pair::new(key, value));
99    }
100
101    pub fn with_description(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
102        self.add_description(key, value);
103        self
104    }
105
106    pub fn set_dependencies(&mut self, deps: Vec<i64>) {
107        self.dependencies = Some(deps);
108    }
109
110    pub fn with_dependencies(mut self, deps: Vec<i64>) -> Self {
111        self.dependencies = Some(deps);
112        self
113    }
114
115    pub fn set_branch_info(&mut self, branch_info: PlanNodeBranchInfo) {
116        self.branch_info = Some(branch_info);
117    }
118
119    pub fn with_branch_info(mut self, branch_info: PlanNodeBranchInfo) -> Self {
120        self.branch_info = Some(branch_info);
121        self
122    }
123
124    pub fn add_profile(&mut self, profile: ProfilingStats) {
125        if self.profiles.is_none() {
126            self.profiles = Some(Vec::new());
127        }
128        self.profiles
129            .as_mut()
130            .expect("profiles should be Some after initialization")
131            .push(profile);
132    }
133}
134
135/// Plan Description
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct PlanDescription {
138    pub plan_node_descs: Vec<PlanNodeDescription>,
139    pub node_index_map: std::collections::HashMap<i64, usize>,
140    pub format: String,
141    pub optimize_time_in_us: i64,
142    pub partition_spec_description: Option<String>,
143
144    // ── parallel execution profile (populated at runtime) ──
145    pub requested_workers: usize,
146    pub actual_workers: usize,
147    pub parallel_wall_time_us: u64,
148    pub parallel_work_time_us: u64,
149    pub parallel_buffered_chunks_peak: usize,
150    pub parallel_buffered_bytes_peak: usize,
151    pub parallel_fallback_reason: String,
152
153    // ── Cost-based decision notes (populated at plan build) ──
154    pub cbo_notes: Vec<String>,
155
156    // ── Statistics settings behind the plan (populated at plan build) ──
157    pub statistics_summary: String,
158
159    // ── Columnar fast-path counters (populated at runtime, PROFILE only) ──
160    pub columnar_summary: String,
161}
162
163impl PlanDescription {
164    pub fn new() -> Self {
165        Self {
166            plan_node_descs: Vec::new(),
167            node_index_map: std::collections::HashMap::new(),
168            format: String::new(),
169            optimize_time_in_us: 0,
170            partition_spec_description: None,
171            requested_workers: 1,
172            actual_workers: 1,
173            parallel_wall_time_us: 0,
174            parallel_work_time_us: 0,
175            parallel_buffered_chunks_peak: 0,
176            parallel_buffered_bytes_peak: 0,
177            parallel_fallback_reason: String::new(),
178            cbo_notes: Vec::new(),
179            statistics_summary: String::new(),
180            columnar_summary: String::new(),
181        }
182    }
183
184    pub fn add_node_desc(&mut self, desc: PlanNodeDescription) -> usize {
185        let index = self.plan_node_descs.len();
186        let node_id = desc.id;
187        self.plan_node_descs.push(desc);
188        self.node_index_map.insert(node_id, index);
189        index
190    }
191
192    pub fn get_node_desc(&self, node_id: i64) -> Option<&PlanNodeDescription> {
193        self.node_index_map
194            .get(&node_id)
195            .and_then(|&index| self.plan_node_descs.get(index))
196    }
197
198    pub fn get_node_desc_mut(&mut self, node_id: i64) -> Option<&mut PlanNodeDescription> {
199        if let Some(&index) = self.node_index_map.get(&node_id) {
200            self.plan_node_descs.get_mut(index)
201        } else {
202            None
203        }
204    }
205}
206
207impl Default for PlanDescription {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn plan_description_p8_fields_defaults() {
219        let desc = PlanDescription::new();
220        assert_eq!(desc.requested_workers, 1);
221        assert_eq!(desc.actual_workers, 1);
222        assert_eq!(desc.parallel_wall_time_us, 0);
223        assert_eq!(desc.parallel_work_time_us, 0);
224        assert_eq!(desc.parallel_buffered_chunks_peak, 0);
225        assert_eq!(desc.parallel_buffered_bytes_peak, 0);
226        assert!(desc.parallel_fallback_reason.is_empty());
227    }
228
229    #[test]
230    fn plan_description_p8_fields_round_trip_serialize() {
231        let mut desc = PlanDescription::new();
232        desc.requested_workers = 4;
233        desc.actual_workers = 2;
234        desc.parallel_wall_time_us = 1000;
235        desc.parallel_work_time_us = 2000;
236        desc.parallel_buffered_chunks_peak = 3;
237        desc.parallel_buffered_bytes_peak = 8192;
238        desc.parallel_fallback_reason = "test fallback".to_string();
239
240        let json = serde_json::to_string(&desc).expect("serialize");
241        let restored: PlanDescription = serde_json::from_str(&json).expect("deserialize");
242
243        assert_eq!(restored.requested_workers, 4);
244        assert_eq!(restored.actual_workers, 2);
245        assert_eq!(restored.parallel_wall_time_us, 1000);
246        assert_eq!(restored.parallel_work_time_us, 2000);
247        assert_eq!(restored.parallel_buffered_chunks_peak, 3);
248        assert_eq!(restored.parallel_buffered_bytes_peak, 8192);
249        assert_eq!(restored.parallel_fallback_reason, "test fallback");
250    }
251}