1use std::fmt::Write as _;
10use uqa_core::Value;
11use uqa_sql::{
12 plan::{QueryBlockPlan, QueryPlan, RelationalPlan, UnifiedPlan},
13 ResultRow, SQLError, SQLResult, ScalarExpr,
14};
15
16pub use uqa_sql::result::ExplainAnalysis;
17
18pub fn run_explain(
19 body: &UnifiedPlan,
20 verbose: bool,
21 format: Option<&str>,
22 analysis: Option<&ExplainAnalysis>,
23) -> Result<SQLResult, SQLError> {
24 let mut plan_text = match body {
25 UnifiedPlan::Query(query) => format_query_plan(query),
26 UnifiedPlan::Command(command) => format!("{}\n {command:#?}", command.name()),
27 };
28 if verbose {
29 plan_text.push_str("\n verbose=true");
30 write!(plan_text, "\n physical_plan={body:#?}")
31 .map_err(|error| SQLError::Internal(format!("format EXPLAIN plan: {error}")))?;
32 }
33 if let Some(analysis) = analysis {
34 let _ = write!(
35 plan_text,
36 "\n actual_rows={}\n affected_rows={}\n execution_time_ms={:.3}",
37 analysis.rows,
38 analysis.affected_rows,
39 analysis.elapsed.as_secs_f64() * 1_000.0
40 );
41 }
42
43 let format = format.unwrap_or("text").to_ascii_lowercase();
44 if format == "json" {
45 let payload = serde_json::json!({
46 "Plan": plan_text.lines().collect::<Vec<_>>(),
47 "Analyze": analysis.is_some(),
48 "Actual Rows": analysis.map(|value| value.rows),
49 "Affected Rows": analysis.map(|value| value.affected_rows),
50 "Execution Time (ms)": analysis.map(|value| value.elapsed.as_secs_f64() * 1_000.0),
51 });
52 let mut row = ResultRow::new();
53 row.insert("plan".to_string(), Value::Str(payload.to_string()));
54 return Ok(SQLResult {
55 kind: uqa_sql::SQLResultKind::Rows,
56 command_tag: None,
57 columns: vec!["plan".to_string()],
58 column_types: vec![Some(uqa_sql::ColumnType::Text)],
59 rows: vec![row],
60 positional_rows: None,
61 affected_rows: 0,
62 });
63 }
64 if format != "text" {
65 return Err(SQLError::Unsupported(format!(
66 "EXPLAIN format `{format}` is not supported; expected TEXT or JSON"
67 )));
68 }
69 let mut rows: Vec<ResultRow> = Vec::new();
70 for line in plan_text.split('\n') {
71 let mut r = ResultRow::new();
72 r.insert("plan".to_string(), Value::Str(line.to_string()));
73 rows.push(r);
74 }
75 Ok(SQLResult {
76 kind: uqa_sql::SQLResultKind::Rows,
77 command_tag: None,
78 columns: vec!["plan".to_string()],
79 column_types: vec![Some(uqa_sql::ColumnType::Text)],
80 rows,
81 positional_rows: None,
82 affected_rows: 0,
83 })
84}
85
86pub fn format_query_plan(plan: &QueryPlan) -> String {
87 match &plan.root {
88 RelationalPlan::QueryBlock(block) => format_select_plan(block),
89 RelationalPlan::SetOp {
90 kind,
91 all,
92 left,
93 right,
94 order_by,
95 limit,
96 offset,
97 ..
98 } => format!(
99 "SetOp\n kind={kind:?}\n all={all}\n left=({})\n right=({})\n order_by={}\n limit={}\n offset={}",
100 format_query_plan(left).replace('\n', "\n "),
101 format_query_plan(right).replace('\n', "\n "),
102 order_by.len(),
103 limit
104 .as_deref()
105 .map_or_else(|| "none".into(), explain_int_expr),
106 offset
107 .as_deref()
108 .map_or_else(|| "none".into(), explain_int_expr),
109 ),
110 RelationalPlan::Values { rows, .. } => format!("Values\n rows={}", rows.len()),
111 }
112}
113
114pub fn format_select_plan(stmt: &QueryBlockPlan) -> String {
115 use std::fmt::Write as _;
116 let mut s = String::new();
117 let _ = writeln!(s, "Select");
118 if !stmt.projections.is_empty() {
119 let _ = writeln!(s, " projections={}", stmt.projections.len());
120 }
121 if let Some(from) = &stmt.from {
122 let _ = writeln!(s, " from={from:?}");
123 }
124 if stmt.r#where.is_some() {
125 let _ = writeln!(s, " where=<expr>");
126 }
127 if !stmt.group_by.is_empty() {
128 let _ = writeln!(s, " group_by={}", stmt.group_by.len());
129 }
130 if !stmt.grouping_sets.is_empty() {
131 let _ = writeln!(s, " grouping_sets={}", stmt.grouping_sets.len());
132 }
133 if !stmt.order_by.is_empty() {
134 let _ = writeln!(s, " order_by={}", stmt.order_by.len());
135 }
136 if let Some(expr) = stmt.limit.as_ref() {
137 let _ = writeln!(s, " limit={}", explain_int_expr(expr));
138 }
139 if let Some(expr) = stmt.offset.as_ref() {
140 let _ = writeln!(s, " offset={}", explain_int_expr(expr));
141 }
142 if stmt.distinct {
143 let _ = writeln!(s, " distinct=true");
144 }
145 if !stmt.locking.is_empty() {
146 let _ = writeln!(s, " locking={}", stmt.locking.len());
147 }
148 s.trim_end().to_string()
149}
150
151pub use uqa_sql::semantics::{select_execution_stmt, should_defer_distinct_limit};
152
153pub fn explain_int_expr(expr: &ScalarExpr) -> String {
154 match expr {
155 ScalarExpr::Literal(Value::Int(n)) => n.to_string(),
156 _ => "<expr>".to_string(),
157 }
158}