Skip to main content

ormdb_proto/
explain.rs

1//! EXPLAIN command result types.
2//!
3//! These types are returned by the EXPLAIN operation to provide
4//! query plan visualization and cost estimation.
5
6use rkyv::{Archive, Deserialize, Serialize};
7use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
8
9/// Result of an EXPLAIN command.
10#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
11pub struct ExplainResult {
12    /// The query plan summary.
13    pub plan: QueryPlanSummary,
14    /// Cost estimates.
15    pub cost: CostSummary,
16    /// Join strategies used for includes.
17    pub joins: Vec<JoinInfo>,
18    /// Whether the plan was found in cache.
19    pub plan_cached: bool,
20    /// Human-readable explanation text.
21    pub explanation: String,
22}
23
24/// Summary of a query plan.
25#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
26pub struct QueryPlanSummary {
27    /// Root entity being queried.
28    pub root_entity: String,
29    /// Fields being projected (empty means all fields).
30    pub fields: Vec<String>,
31    /// Filter description (if any).
32    pub filter_description: Option<String>,
33    /// Filter selectivity estimate (0.0-1.0).
34    pub filter_selectivity: Option<f64>,
35    /// Include plans for related entities.
36    pub includes: Vec<IncludeSummary>,
37    /// Fanout budget constraints.
38    pub budget: BudgetSummary,
39    /// Ordering specification.
40    pub order_by: Vec<OrderSummary>,
41    /// Pagination parameters.
42    pub pagination: Option<PaginationSummary>,
43}
44
45/// Summary of an include plan.
46#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
47pub struct IncludeSummary {
48    /// Path like "posts" or "posts.comments".
49    pub path: String,
50    /// Target entity type.
51    pub target_entity: String,
52    /// Relation type (OneToOne, OneToMany, ManyToMany).
53    pub relation_type: String,
54    /// Depth level (1 for top-level, 2 for nested, etc.).
55    pub depth: u32,
56    /// Estimated rows from this include.
57    pub estimated_rows: u64,
58    /// Filter on this include (if any).
59    pub filter_description: Option<String>,
60}
61
62/// Cost summary for the query.
63#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
64pub struct CostSummary {
65    /// Estimated total rows returned.
66    pub estimated_rows: u64,
67    /// Estimated I/O operations (scans, lookups).
68    pub io_cost: u64,
69    /// Estimated CPU cost (filter evaluations, comparisons).
70    pub cpu_cost: u64,
71    /// Total weighted cost (io_cost * 10 + cpu_cost).
72    pub total_cost: f64,
73}
74
75/// Information about a join strategy.
76#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
77pub struct JoinInfo {
78    /// Relation path being joined.
79    pub path: String,
80    /// Join strategy selected.
81    pub strategy: JoinStrategyType,
82    /// Reason for selecting this strategy.
83    pub reason: String,
84    /// Estimated parent count.
85    pub parent_count: u64,
86    /// Estimated child count.
87    pub child_count: u64,
88}
89
90/// Join strategy type.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
92pub enum JoinStrategyType {
93    /// Nested loop join - for small datasets.
94    NestedLoop,
95    /// Hash join - for larger datasets.
96    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/// Budget summary.
109#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
110pub struct BudgetSummary {
111    /// Maximum entities allowed.
112    pub max_entities: u64,
113    /// Maximum edges allowed.
114    pub max_edges: u64,
115    /// Maximum depth allowed.
116    pub max_depth: u32,
117}
118
119/// Order by specification summary.
120#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
121pub struct OrderSummary {
122    /// Field being ordered by.
123    pub field: String,
124    /// Direction (ASC or DESC).
125    pub direction: String,
126}
127
128/// Pagination summary.
129#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize, SerdeSerialize, SerdeDeserialize)]
130pub struct PaginationSummary {
131    /// Maximum rows to return.
132    pub limit: Option<u64>,
133    /// Rows to skip.
134    pub offset: Option<u64>,
135}
136
137impl ExplainResult {
138    /// Create a new explain result.
139    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    /// Create a new query plan summary.
158    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    /// Set the fields to project.
172    pub fn with_fields(mut self, fields: Vec<String>) -> Self {
173        self.fields = fields;
174        self
175    }
176
177    /// Set the filter description.
178    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    /// Add an include.
185    pub fn with_include(mut self, include: IncludeSummary) -> Self {
186        self.includes.push(include);
187        self
188    }
189
190    /// Set the budget.
191    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    /// Create a new cost summary.
209    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    /// Create zero cost (for when statistics are unavailable).
220    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    /// Create a new include summary.
232    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    /// Set estimated rows.
249    pub fn with_estimated_rows(mut self, rows: u64) -> Self {
250        self.estimated_rows = rows;
251        self
252    }
253
254    /// Set filter description.
255    pub fn with_filter(mut self, description: String) -> Self {
256        self.filter_description = Some(description);
257        self
258    }
259}
260
261impl JoinInfo {
262    /// Create a new join info.
263    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        // total = 1500 * 10 + 200 = 15200
318        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}