Skip to main content

uqa_planner/
explain.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Explain result rendering and query-block limit placement.
8
9use 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}