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;
17use uqa_sql::result::ExplainPhysicalPlan;
18
19mod physical;
20mod vector;
21pub use physical::{physical_plan, PhysicalExplainContext};
22
23fn render_generation(generation: uqa_core::VectorGeneration) -> serde_json::Value {
24 let mut database = String::with_capacity(32);
25 for byte in generation.database {
26 let _ = write!(database, "{byte:02x}");
27 }
28 serde_json::json!({"Database": database, "Table": generation.table, "Index": generation.index, "Generation": generation.generation})
29}
30
31pub fn run_explain(
32 body: &UnifiedPlan,
33 verbose: bool,
34 format: Option<&str>,
35 analysis: Option<&ExplainAnalysis>,
36) -> Result<SQLResult, SQLError> {
37 run_explain_with_physical(
38 body,
39 verbose,
40 format,
41 analysis,
42 &ExplainPhysicalPlan::default(),
43 )
44}
45
46pub fn run_explain_with_physical(
47 body: &UnifiedPlan,
48 verbose: bool,
49 format: Option<&str>,
50 analysis: Option<&ExplainAnalysis>,
51 physical: &ExplainPhysicalPlan,
52) -> Result<SQLResult, SQLError> {
53 let format = format.unwrap_or("text").to_ascii_lowercase();
54 let mut plan_text = match body {
55 UnifiedPlan::Query(query) => format_query_plan(query),
56 UnifiedPlan::Command(command) => format!("{}\n {command:#?}", command.name()),
57 };
58 if verbose {
59 plan_text.push_str("\n verbose=true");
60 write!(plan_text, "\n physical_plan={body:#?}")
61 .map_err(|error| SQLError::Internal(format!("format EXPLAIN plan: {error}")))?;
62 }
63 if format == "text" {
64 for node in &physical.nodes {
65 let rendered = serde_json::to_string_pretty(node)
66 .map_err(|error| SQLError::Internal(format!("format physical EXPLAIN: {error}")))?;
67 for line in rendered.lines() {
68 let _ = write!(plan_text, "\n {line}");
69 }
70 }
71 }
72 if let Some(analysis) = analysis {
73 if format == "text" {
74 vector::append_text(&mut plan_text, analysis)?;
75 }
76 let _ = write!(
77 plan_text,
78 "\n actual_rows={}\n affected_rows={}\n execution_time_ms={:.3}",
79 analysis.rows,
80 analysis.affected_rows,
81 analysis.elapsed.as_secs_f64() * 1_000.0
82 );
83 }
84
85 if format == "json" {
86 let mut payload = serde_json::json!({
87 "Plan": plan_text.lines().collect::<Vec<_>>(),
88 "Analyze": analysis.is_some(),
89 "Actual Rows": analysis.map(|value| value.rows),
90 "Affected Rows": analysis.map(|value| value.affected_rows),
91 "Execution Time (ms)": analysis.map(|value| value.elapsed.as_secs_f64() * 1_000.0),
92 });
93 if let Some(analysis) = analysis {
94 payload["Vector Searches"] = serde_json::Value::Array(
95 analysis
96 .vector_searches
97 .iter()
98 .map(|search| vector::record(search))
99 .collect(),
100 );
101 }
102 if !physical.nodes.is_empty() {
103 payload["Physical Plans"] = serde_json::Value::Array(physical.nodes.clone());
104 }
105 let mut row = ResultRow::new();
106 row.insert("plan".to_string(), Value::Str(payload.to_string()));
107 return Ok(SQLResult {
108 kind: uqa_sql::SQLResultKind::Rows,
109 command_tag: None,
110 columns: vec!["plan".to_string()],
111 column_types: vec![Some(uqa_sql::ColumnType::Text)],
112 rows: vec![row],
113 positional_rows: None,
114 affected_rows: 0,
115 });
116 }
117 if format != "text" {
118 return Err(SQLError::Unsupported(format!(
119 "EXPLAIN format `{format}` is not supported; expected TEXT or JSON"
120 )));
121 }
122 let mut rows: Vec<ResultRow> = Vec::new();
123 for line in plan_text.split('\n') {
124 let mut r = ResultRow::new();
125 r.insert("plan".to_string(), Value::Str(line.to_string()));
126 rows.push(r);
127 }
128 Ok(SQLResult {
129 kind: uqa_sql::SQLResultKind::Rows,
130 command_tag: None,
131 columns: vec!["plan".to_string()],
132 column_types: vec![Some(uqa_sql::ColumnType::Text)],
133 rows,
134 positional_rows: None,
135 affected_rows: 0,
136 })
137}
138
139pub fn format_query_plan(plan: &QueryPlan) -> String {
140 match &plan.root {
141 RelationalPlan::QueryBlock(block) => format_select_plan(block),
142 RelationalPlan::SetOp {
143 kind,
144 all,
145 left,
146 right,
147 order_by,
148 limit,
149 offset,
150 ..
151 } => format!(
152 "SetOp\n kind={kind:?}\n all={all}\n left=({})\n right=({})\n order_by={}\n limit={}\n offset={}",
153 format_query_plan(left).replace('\n', "\n "),
154 format_query_plan(right).replace('\n', "\n "),
155 order_by.len(),
156 limit
157 .as_deref()
158 .map_or_else(|| "none".into(), explain_int_expr),
159 offset
160 .as_deref()
161 .map_or_else(|| "none".into(), explain_int_expr),
162 ),
163 RelationalPlan::Values { rows, .. } => format!("Values\n rows={}", rows.len()),
164 }
165}
166
167pub fn format_select_plan(stmt: &QueryBlockPlan) -> String {
168 use std::fmt::Write as _;
169 let mut s = String::new();
170 let _ = writeln!(s, "Select");
171 if !stmt.projections.is_empty() {
172 let _ = writeln!(s, " projections={}", stmt.projections.len());
173 }
174 if let Some(from) = &stmt.from {
175 let _ = writeln!(s, " from={from:?}");
176 }
177 if stmt.r#where.is_some() {
178 let _ = writeln!(s, " where=<expr>");
179 }
180 if !stmt.group_by.is_empty() {
181 let _ = writeln!(s, " group_by={}", stmt.group_by.len());
182 }
183 if !stmt.grouping_sets.is_empty() {
184 let _ = writeln!(s, " grouping_sets={}", stmt.grouping_sets.len());
185 }
186 if !stmt.order_by.is_empty() {
187 let _ = writeln!(s, " order_by={}", stmt.order_by.len());
188 }
189 if let Some(expr) = stmt.limit.as_ref() {
190 let _ = writeln!(s, " limit={}", explain_int_expr(expr));
191 }
192 if let Some(expr) = stmt.offset.as_ref() {
193 let _ = writeln!(s, " offset={}", explain_int_expr(expr));
194 }
195 if stmt.distinct {
196 let _ = writeln!(s, " distinct=true");
197 }
198 if !stmt.locking.is_empty() {
199 let _ = writeln!(s, " locking={}", stmt.locking.len());
200 }
201 s.trim_end().to_string()
202}
203
204pub use uqa_sql::semantics::{select_execution_stmt, should_defer_distinct_limit};
205
206pub fn explain_int_expr(expr: &ScalarExpr) -> String {
207 match expr {
208 ScalarExpr::Literal(Value::Int(n)) => n.to_string(),
209 _ => "<expr>".to_string(),
210 }
211}