diesel_pg_explain/
lib.rs

1//! A lightweight utility for wrapping Diesel queries with `EXPLAIN (FORMAT JSON)`
2//! and parsing the resulting execution plan into a structured Rust type.
3//!
4//! This crate is intended for use with PostgreSQL and the Diesel ORM.
5//! It provides a simple API to introspect query plans programmatically,
6//! enabling query diagnostics, optimization tools, or logging systems.
7//!
8//! # Features
9//!
10//! - Wraps any Diesel query using `EXPLAIN (FORMAT JSON)`
11//! - Parses the JSON output into a typed `ExplainPlan` structure
12//! - Compatible with Diesel's `QueryDsl` and `RunQueryDsl`
13//! - Deserialization errors are reported as standard Diesel errors
14//!
15//! # Example
16//!
17//! ```rust
18//! use diesel::prelude::*;
19//! use diesl_pg_explain::{ExplainWrapped, ExplainPlan};
20//!
21//! let connection = &mut establish_connection();
22//! let query = users::table.filter(users::name.like("%example%"));
23//!
24//! let plan: ExplainPlan = query.wrap_explain().explain(connection)?;
25//! println!("{:#?}", plan);
26//! ```
27//!
28//! # Integration
29//!
30//! This crate is best used in development tooling, diagnostics dashboards,
31//! or CLI utilities where understanding PostgreSQL query plans is helpful.
32//!
33//! Note: this does not run the actual query — it only asks PostgreSQL to
34//! generate and return the execution plan.
35//!
36//! # See also
37//!
38//! - [PostgreSQL EXPLAIN documentation](https://www.postgresql.org/docs/current/using-explain.html)
39//!
40//! # Crate Features
41//!
42//! Currently no optional features. May add feature gates for serde or Diesel version in the future.
43
44use diesel::pg::{Pg, PgConnection};
45use diesel::prelude::*;
46use diesel::query_builder::*;
47use diesel::query_dsl::methods::LoadQuery;
48use serde::{Deserialize, Serialize};
49
50/// Recursive struct which describes the plan of a query
51#[derive(Debug, Serialize, Deserialize)]
52pub struct ExplainPlan {
53    /// The type of the plan node (e.g., "Seq Scan", "Nested Loop", "Hash Join").
54    /// Indicates the operation performed at this step in the query execution plan.
55    #[serde(rename = "Node Type")]
56    pub node_type: String,
57
58    /// The relationship of this node to its parent in the plan tree.
59    /// Common values include:
60    /// - "Outer": This node is the outer input to a join (e.g., Nested Loop).
61    /// - "Inner": This node is the inner input to a join.
62    /// - "Subquery": This node is part of a subquery.
63    /// - "InitPlan", "SubPlan", "Member": Special plan node roles.
64    ///
65    /// May be `None` for root nodes or when not applicable.
66    #[serde(rename = "Parent Relationship", default)]
67    pub parent_relationship: Option<String>,
68
69    /// Indicates whether the plan node is aware of parallel query execution.
70    /// If true, the node may participate in or benefit from parallelism.
71    #[serde(rename = "Parallel Aware")]
72    pub parallel_aware: bool,
73
74    /// Indicates whether the node supports asynchronous execution.
75    /// Async-capable nodes can execute operations concurrently with others,
76    /// improving performance in some plans (especially with I/O or remote sources).
77    #[serde(rename = "Async Capable")]
78    pub async_capable: bool,
79
80    /// The estimated cost of starting this plan node.
81    /// This typically includes one-time setup costs, like initializing data structures.
82    #[serde(rename = "Startup Cost")]
83    pub startup_cost: f64,
84
85    /// The estimated total cost of fully executing this plan node,
86    /// including startup and all tuple processing.
87    #[serde(rename = "Total Cost")]
88    pub total_cost: f64,
89
90    /// The estimated number of rows this plan node will output.
91    /// This is a planner estimate, not an actual runtime value.
92    #[serde(rename = "Plan Rows")]
93    pub plan_rows: u64,
94
95    /// The estimated average width (in bytes) of each row produced by this node.
96    /// Useful for understanding memory and I/O implications.
97    #[serde(rename = "Plan Width")]
98    pub plan_width: u64,
99
100    /// Child plan nodes that this node depends on or drives.
101    /// For example, a join node will typically have two child plans (inner and outer).
102    #[serde(rename = "Plans", default)]
103    pub plans: Vec<ExplainPlan>,
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107struct ExplainItem {
108    #[serde(rename = "Plan")]
109    pub plan: ExplainPlan,
110}
111
112/// A wrapper around a Diesel query that transforms it into an
113/// `EXPLAIN (FORMAT JSON)` query.
114///
115/// Use this type to inspect the query execution plan without running the query.
116///
117/// Example:
118/// ```rust
119/// let plan = my_query.wrap_explain().explain(&mut conn)?;
120/// println!("{:#?}", plan);
121/// ```
122#[derive(Clone, Copy, QueryId)]
123pub struct Explain<Q>(pub Q);
124
125impl<Q> QueryFragment<Pg> for Explain<Q>
126where
127    Q: QueryFragment<Pg>,
128{
129    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> diesel::result::QueryResult<()> {
130        out.push_sql("EXPLAIN (FORMAT JSON) ");
131        self.0.walk_ast(out.reborrow())?;
132        Ok(())
133    }
134}
135
136impl<Q: Query> Query for Explain<Q> {
137    type SqlType = diesel::sql_types::Text;
138}
139
140impl<Q> RunQueryDsl<PgConnection> for Explain<Q> {}
141
142impl<Q> Explain<Q> {
143    /// Executes the wrapped query using `EXPLAIN (FORMAT JSON)`, parses the result,
144    /// and returns a structured `ExplainPlan` that represents the root of the query plan tree.
145    ///
146    /// # Errors
147    /// Returns a `diesel::result::Error::DeserializationError` if the JSON returned
148    /// by PostgreSQL cannot be parsed into an `ExplainPlan`.
149    pub fn explain<'a>(self, conn: &mut PgConnection) -> QueryResult<ExplainPlan>
150    where
151        Self: LoadQuery<'a, PgConnection, String>,
152    {
153        let r = self.load::<String>(conn)?.into_iter().next().unwrap();
154
155        let r: Vec<ExplainItem> = serde_json::from_str(&r).map_err(|e: serde_json::Error| {
156            diesel::result::Error::DeserializationError(Box::new(e))
157        })?;
158        let r = r.into_iter().next().unwrap().plan;
159        Ok(r)
160    }
161}
162
163/// A trait that allows any Diesel query to be wrapped
164/// in an `EXPLAIN (FORMAT JSON)` call using the [`Explain`] wrapper.
165///
166/// This is implemented for all query types.
167pub trait ExplainWrapped: Sized {
168    /// Wraps the query into an `EXPLAIN` wrapper, allowing it to be analyzed
169    /// using [`Explain::explain()`].
170    ///
171    /// Example:
172    /// ```rust
173    /// use diesel_pg_explain::ExplainWrapped;
174    /// let explained = query.wrap_explain();
175    /// ```
176    fn wrap_explain(&self) -> Explain<&Self>;
177}
178
179impl<Q> ExplainWrapped for Q {
180    fn wrap_explain(&self) -> Explain<&Self> {
181        Explain(self)
182    }
183}