use super::{run_on, ExecError, RowRef, SqlExecutor};
use crate::core::SelectQuery;
use crate::sql::Pool;
#[derive(Debug, Clone, Default)]
pub struct ExplainOptions {
pub analyze: bool,
pub buffers: bool,
pub verbose: bool,
pub format: ExplainFormat,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ExplainFormat {
#[default]
Text,
Json,
Yaml,
Xml,
}
impl ExplainOptions {
fn to_clause(&self) -> String {
let mut bits: Vec<&'static str> = Vec::new();
if self.analyze {
bits.push("ANALYZE");
}
if self.buffers {
bits.push("BUFFERS");
}
if self.verbose {
bits.push("VERBOSE");
}
let format_bit = match self.format {
ExplainFormat::Text => None,
ExplainFormat::Json => Some("FORMAT JSON"),
ExplainFormat::Yaml => Some("FORMAT YAML"),
ExplainFormat::Xml => Some("FORMAT XML"),
};
if let Some(f) = format_bit {
bits.push(f);
}
if bits.is_empty() {
String::new()
} else {
format!("({})", bits.join(", "))
}
}
}
pub async fn explain_pool(
pool: &Pool,
query: &SelectQuery,
options: ExplainOptions,
) -> Result<String, ExecError> {
Ok(explain_lines_on(pool, query, &options).await?.join("\n"))
}
pub(super) async fn explain_lines_on<'c, E: SqlExecutor<'c>>(
exec: E,
query: &SelectQuery,
options: &ExplainOptions,
) -> Result<Vec<String>, ExecError> {
let d = exec.dialect();
let stmt = d.compile_select(query)?;
let json = options.format == ExplainFormat::Json;
let prefix = match d.name() {
"mysql" if options.analyze => "EXPLAIN ANALYZE ".to_owned(),
"mysql" if json => "EXPLAIN FORMAT=JSON ".to_owned(),
"mysql" => "EXPLAIN FORMAT=TREE ".to_owned(),
"sqlite" => "EXPLAIN QUERY PLAN ".to_owned(),
_ => match options.to_clause() {
c if c.is_empty() => "EXPLAIN ".to_owned(),
c => format!("EXPLAIN {c} "),
},
};
let sql = format!("{prefix}{}", stmt.sql);
let lines = run_on(exec, &sql, stmt.params, |row| match row {
#[cfg(feature = "postgres")]
RowRef::Postgres(r) => first_cell(r, json),
#[cfg(feature = "mysql")]
RowRef::Mysql(r) => first_cell(r, json && !options.analyze),
#[cfg(feature = "sqlite")]
RowRef::Sqlite(r) => {
use sqlx::Row as _;
let detail: String = r.try_get(3)?;
if !json {
return Ok(detail);
}
let (id, parent): (i64, i64) = (r.try_get(0)?, r.try_get(1)?);
Ok(serde_json::json!({"id": id, "parent": parent, "detail": detail}).to_string())
}
})
.await?;
if json && d.name() == "sqlite" {
return Ok(vec![format!("[{}]", lines.join(","))]);
}
Ok(lines)
}
#[cfg(any(feature = "postgres", feature = "mysql"))]
fn first_cell<R>(row: &R, json: bool) -> Result<String, sqlx::Error>
where
R: sqlx::Row,
usize: sqlx::ColumnIndex<R>,
String: for<'r> sqlx::Decode<'r, R::Database> + sqlx::Type<R::Database>,
serde_json::Value: for<'r> sqlx::Decode<'r, R::Database> + sqlx::Type<R::Database>,
{
if json {
Ok(row.try_get::<serde_json::Value, _>(0)?.to_string())
} else {
row.try_get(0)
}
}