dbmcp_postgres/tools/
explain_query.rs1use std::borrow::Cow;
4
5use dbmcp_server::types::{ExplainQueryRequest, QueryResponse};
6use dbmcp_sql::Connection as _;
7use dbmcp_sql::validation::validate_read_only;
8use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
9use rmcp::model::{ErrorData, ToolAnnotations};
10
11use crate::PostgresHandler;
12
13pub(crate) struct ExplainQueryTool;
15
16impl ExplainQueryTool {
17 const NAME: &'static str = "explainQuery";
18 const TITLE: &'static str = "Explain Query";
19 const DESCRIPTION: &'static str = r#"Return the execution plan for a SQL query to diagnose performance. Use this tool instead of running EXPLAIN directly through readQuery — it provides structured JSON output.
20
21<usecase>
22Use when:
23- A query runs slowly and you need to understand why
24- Investigating performance bottlenecks
25- Planning index creation to optimize queries
26- Analyzing join methods, table scan strategies, and sort operations
27</usecase>
28
29<when_not_to_use>
30- Running actual queries → use readQuery or writeQuery
31- Checking table structure → use listTables(detailed=true)
32</when_not_to_use>
33
34<examples>
35✓ "Why is my SELECT on orders slow?" → explainQuery(query="SELECT ...")
36✓ "Should I add an index?" → explainQuery with analyze=true
37✗ "Run this SELECT" → use readQuery
38</examples>
39
40<safety>
41Set `analyze` to true for actual execution statistics (EXPLAIN ANALYZE).
42IMPORTANT: EXPLAIN ANALYZE actually executes the query! In read-only mode, only read-only statements are allowed with analyze.
43When analyze is false, returns EXPLAIN (FORMAT JSON) output without executing.
44</safety>
45
46<what_it_returns>
47A JSON array of execution plan rows showing access methods, join types, row estimates, and costs.
48</what_it_returns>"#;
49}
50
51impl ToolBase for ExplainQueryTool {
52 type Parameter = ExplainQueryRequest;
53 type Output = QueryResponse;
54 type Error = ErrorData;
55
56 fn name() -> Cow<'static, str> {
57 Self::NAME.into()
58 }
59
60 fn title() -> Option<String> {
61 Some(Self::TITLE.into())
62 }
63
64 fn description() -> Option<Cow<'static, str>> {
65 Some(Self::DESCRIPTION.into())
66 }
67
68 fn annotations() -> Option<ToolAnnotations> {
69 Some(
70 ToolAnnotations::new()
71 .read_only(true)
72 .destructive(false)
73 .idempotent(true)
74 .open_world(true),
75 )
76 }
77}
78
79impl AsyncTool<PostgresHandler> for ExplainQueryTool {
80 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
81 handler.explain_query(params).await
82 }
83}
84
85impl PostgresHandler {
86 pub async fn explain_query(
97 &self,
98 ExplainQueryRequest {
99 database,
100 query,
101 analyze,
102 }: ExplainQueryRequest,
103 ) -> Result<QueryResponse, ErrorData> {
104 if analyze && self.config.read_only {
105 let _ = validate_read_only(&query, &sqlparser::dialect::PostgreSqlDialect {})?;
106 }
107
108 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
109
110 let explain_sql = if analyze {
111 format!("EXPLAIN (ANALYZE, FORMAT JSON) {query}")
112 } else {
113 format!("EXPLAIN (FORMAT JSON) {query}")
114 };
115
116 let mut rows = self.connection.fetch_json(explain_sql.as_str(), database).await?;
117 if let Some(r) = &self.redactor {
118 r.apply(&mut rows)?;
119 }
120
121 Ok(QueryResponse { rows })
122 }
123}