1use rkyv::{Archive, Deserialize, Serialize};
7use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
8
9#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
11pub struct ExplainResult {
12 pub plan: QueryPlanSummary,
14 pub cost: CostSummary,
16 pub joins: Vec<JoinInfo>,
18 pub plan_cached: bool,
20 pub explanation: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
26pub struct QueryPlanSummary {
27 pub root_entity: String,
29 pub fields: Vec<String>,
31 pub filter_description: Option<String>,
33 pub filter_selectivity: Option<f64>,
35 pub includes: Vec<IncludeSummary>,
37 pub budget: BudgetSummary,
39 pub order_by: Vec<OrderSummary>,
41 pub pagination: Option<PaginationSummary>,
43}
44
45#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
47pub struct IncludeSummary {
48 pub path: String,
50 pub target_entity: String,
52 pub relation_type: String,
54 pub depth: u32,
56 pub estimated_rows: u64,
58 pub filter_description: Option<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
64pub struct CostSummary {
65 pub estimated_rows: u64,
67 pub io_cost: u64,
69 pub cpu_cost: u64,
71 pub total_cost: f64,
73}
74
75#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
77pub struct JoinInfo {
78 pub path: String,
80 pub strategy: JoinStrategyType,
82 pub reason: String,
84 pub parent_count: u64,
86 pub child_count: u64,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
92pub enum JoinStrategyType {
93 NestedLoop,
95 HashJoin,
97}
98
99impl std::fmt::Display for JoinStrategyType {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 JoinStrategyType::NestedLoop => write!(f, "NestedLoop"),
103 JoinStrategyType::HashJoin => write!(f, "HashJoin"),
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
110pub struct BudgetSummary {
111 pub max_entities: u64,
113 pub max_edges: u64,
115 pub max_depth: u32,
117}
118
119#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
121pub struct OrderSummary {
122 pub field: String,
124 pub direction: String,
126}
127
128#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
130pub struct PaginationSummary {
131 pub limit: Option<u64>,
133 pub offset: Option<u64>,
135}
136
137impl ExplainResult {
138 pub fn new(
140 plan: QueryPlanSummary,
141 cost: CostSummary,
142 joins: Vec<JoinInfo>,
143 plan_cached: bool,
144 explanation: String,
145 ) -> Self {
146 Self {
147 plan,
148 cost,
149 joins,
150 plan_cached,
151 explanation,
152 }
153 }
154}
155
156impl QueryPlanSummary {
157 pub fn new(root_entity: impl Into<String>) -> Self {
159 Self {
160 root_entity: root_entity.into(),
161 fields: Vec::new(),
162 filter_description: None,
163 filter_selectivity: None,
164 includes: Vec::new(),
165 budget: BudgetSummary::default(),
166 order_by: Vec::new(),
167 pagination: None,
168 }
169 }
170
171 pub fn with_fields(mut self, fields: Vec<String>) -> Self {
173 self.fields = fields;
174 self
175 }
176
177 pub fn with_filter(mut self, description: String, selectivity: f64) -> Self {
179 self.filter_description = Some(description);
180 self.filter_selectivity = Some(selectivity);
181 self
182 }
183
184 pub fn with_include(mut self, include: IncludeSummary) -> Self {
186 self.includes.push(include);
187 self
188 }
189
190 pub fn with_budget(mut self, budget: BudgetSummary) -> Self {
192 self.budget = budget;
193 self
194 }
195}
196
197impl Default for BudgetSummary {
198 fn default() -> Self {
199 Self {
200 max_entities: 10_000,
201 max_edges: 50_000,
202 max_depth: 5,
203 }
204 }
205}
206
207impl CostSummary {
208 pub fn new(estimated_rows: u64, io_cost: u64, cpu_cost: u64) -> Self {
210 let total_cost = (io_cost as f64 * 10.0) + (cpu_cost as f64);
211 Self {
212 estimated_rows,
213 io_cost,
214 cpu_cost,
215 total_cost,
216 }
217 }
218
219 pub fn zero() -> Self {
221 Self {
222 estimated_rows: 0,
223 io_cost: 0,
224 cpu_cost: 0,
225 total_cost: 0.0,
226 }
227 }
228}
229
230impl IncludeSummary {
231 pub fn new(
233 path: impl Into<String>,
234 target_entity: impl Into<String>,
235 relation_type: impl Into<String>,
236 depth: u32,
237 ) -> Self {
238 Self {
239 path: path.into(),
240 target_entity: target_entity.into(),
241 relation_type: relation_type.into(),
242 depth,
243 estimated_rows: 0,
244 filter_description: None,
245 }
246 }
247
248 pub fn with_estimated_rows(mut self, rows: u64) -> Self {
250 self.estimated_rows = rows;
251 self
252 }
253
254 pub fn with_filter(mut self, description: String) -> Self {
256 self.filter_description = Some(description);
257 self
258 }
259}
260
261impl JoinInfo {
262 pub fn new(
264 path: impl Into<String>,
265 strategy: JoinStrategyType,
266 reason: impl Into<String>,
267 parent_count: u64,
268 child_count: u64,
269 ) -> Self {
270 Self {
271 path: path.into(),
272 strategy,
273 reason: reason.into(),
274 parent_count,
275 child_count,
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_explain_result_serialization() {
286 let result = ExplainResult::new(
287 QueryPlanSummary::new("User")
288 .with_fields(vec!["id".into(), "name".into()])
289 .with_filter("status == 'active'".into(), 0.1),
290 CostSummary::new(100, 1500, 200),
291 vec![JoinInfo::new(
292 "posts",
293 JoinStrategyType::HashJoin,
294 "Large dataset",
295 100,
296 5000,
297 )],
298 false,
299 "Query Plan for User".into(),
300 );
301
302 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&result).unwrap();
303 let archived =
304 rkyv::access::<ArchivedExplainResult, rkyv::rancor::Error>(&bytes).unwrap();
305 let deserialized: ExplainResult =
306 rkyv::deserialize::<ExplainResult, rkyv::rancor::Error>(archived).unwrap();
307
308 assert_eq!(result, deserialized);
309 }
310
311 #[test]
312 fn test_cost_summary() {
313 let cost = CostSummary::new(100, 1500, 200);
314 assert_eq!(cost.estimated_rows, 100);
315 assert_eq!(cost.io_cost, 1500);
316 assert_eq!(cost.cpu_cost, 200);
317 assert!((cost.total_cost - 15200.0).abs() < 0.01);
319 }
320
321 #[test]
322 fn test_join_strategy_display() {
323 assert_eq!(format!("{}", JoinStrategyType::NestedLoop), "NestedLoop");
324 assert_eq!(format!("{}", JoinStrategyType::HashJoin), "HashJoin");
325 }
326}