use std::fmt::{self, Display};
use std::str::FromStr;
use arrow::compute::CastOptions;
use arrow::util::display::{DurationFormat, FormatOptions};
use crate::config::{ConfigField, Visit};
use crate::error::{DataFusionError, Result};
#[cfg(feature = "sql")]
use sqlparser::ast::{Expr, UtilityOption, Value, ValueWithSpan};
pub const DEFAULT_FORMAT_OPTIONS: FormatOptions<'static> =
FormatOptions::new().with_duration_format(DurationFormat::Pretty);
pub const DEFAULT_CAST_OPTIONS: CastOptions<'static> = CastOptions {
safe: false,
format_options: DEFAULT_FORMAT_OPTIONS,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExplainFormat {
Indent,
Tree,
PostgresJSON,
Graphviz,
}
impl FromStr for ExplainFormat {
type Err = DataFusionError;
fn from_str(format: &str) -> Result<Self, Self::Err> {
match format.to_lowercase().as_str() {
"indent" => Ok(ExplainFormat::Indent),
"tree" => Ok(ExplainFormat::Tree),
"pgjson" => Ok(ExplainFormat::PostgresJSON),
"graphviz" => Ok(ExplainFormat::Graphviz),
_ => Err(DataFusionError::Configuration(format!(
"Invalid explain format. Expected 'indent', 'tree', 'pgjson' or 'graphviz'. Got '{format}'"
))),
}
}
}
impl Display for ExplainFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ExplainFormat::Indent => "indent",
ExplainFormat::Tree => "tree",
ExplainFormat::PostgresJSON => "pgjson",
ExplainFormat::Graphviz => "graphviz",
};
write!(f, "{s}")
}
}
impl ConfigField for ExplainFormat {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, value: &str) -> Result<()> {
*self = ExplainFormat::from_str(value)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MetricType {
Summary,
Dev,
}
impl MetricType {
pub fn included_types(self) -> Vec<MetricType> {
match self {
MetricType::Summary => vec![MetricType::Summary],
MetricType::Dev => vec![MetricType::Summary, MetricType::Dev],
}
}
}
impl FromStr for MetricType {
type Err = DataFusionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"summary" => Ok(Self::Summary),
"dev" => Ok(Self::Dev),
other => Err(DataFusionError::Configuration(format!(
"Invalid explain analyze level. Expected 'summary' or 'dev'. Got '{other}'"
))),
}
}
}
impl Display for MetricType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Summary => write!(f, "summary"),
Self::Dev => write!(f, "dev"),
}
}
}
impl ConfigField for MetricType {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, value: &str) -> Result<()> {
*self = MetricType::from_str(value)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MetricCategory {
Rows,
Bytes,
Timing,
Uncategorized,
}
impl FromStr for MetricCategory {
type Err = DataFusionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"rows" => Ok(Self::Rows),
"bytes" => Ok(Self::Bytes),
"timing" => Ok(Self::Timing),
"uncategorized" => Ok(Self::Uncategorized),
other => Err(DataFusionError::Configuration(format!(
"Invalid metric category '{other}'. \
Expected 'rows', 'bytes', 'timing', or 'uncategorized'."
))),
}
}
}
impl Display for MetricCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Rows => write!(f, "rows"),
Self::Bytes => write!(f, "bytes"),
Self::Timing => write!(f, "timing"),
Self::Uncategorized => write!(f, "uncategorized"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub enum ExplainAnalyzeCategories {
#[default]
All,
Only(Vec<MetricCategory>),
}
impl FromStr for ExplainAnalyzeCategories {
type Err = DataFusionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim().to_lowercase();
match s.as_str() {
"all" => Ok(Self::All),
"none" => Ok(Self::Only(vec![])),
other => {
let mut cats = Vec::new();
for part in other.split(',') {
cats.push(part.trim().parse::<MetricCategory>()?);
}
cats.dedup();
Ok(Self::Only(cats))
}
}
}
}
impl Display for ExplainAnalyzeCategories {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::All => write!(f, "all"),
Self::Only(cats) if cats.is_empty() => write!(f, "none"),
Self::Only(cats) => {
let mut first = true;
for cat in cats {
if !first {
write!(f, ",")?;
}
first = false;
write!(f, "{cat}")?;
}
Ok(())
}
}
}
}
impl ConfigField for ExplainAnalyzeCategories {
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
v.some(key, self, description)
}
fn set(&mut self, _: &str, value: &str) -> Result<()> {
*self = ExplainAnalyzeCategories::from_str(value)?;
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct ExplainStatementOptions {
pub analyze: bool,
pub verbose: bool,
pub format: Option<ExplainFormat>,
pub analyze_level: Option<MetricType>,
pub analyze_categories: Option<ExplainAnalyzeCategories>,
pub show_statistics: Option<bool>,
}
#[cfg(feature = "sql")]
impl ExplainStatementOptions {
pub fn from_utility_options(opts: &[UtilityOption]) -> Result<Self> {
let mut out = ExplainStatementOptions::default();
let mut metrics_explicit = false;
for opt in opts {
let name = opt.name.value.to_ascii_lowercase();
match name.as_str() {
"analyze" => {
out.analyze = parse_bool_arg(&opt.arg, &name)?;
}
"verbose" => {
out.verbose = parse_bool_arg(&opt.arg, &name)?;
}
"format" => {
let s = parse_ident_or_string_arg(&opt.arg, &name)?;
out.format = Some(ExplainFormat::from_str(&s)?);
}
"metrics" => {
let s = parse_ident_or_string_arg(&opt.arg, &name)?;
out.analyze_categories =
Some(ExplainAnalyzeCategories::from_str(&s)?);
metrics_explicit = true;
}
"level" => {
let s = parse_ident_or_string_arg(&opt.arg, &name)?;
out.analyze_level = Some(MetricType::from_str(&s)?);
}
"timing" => {
let enable = parse_bool_arg(&opt.arg, &name)?;
out.analyze_categories = Some(adjust_timing(
out.analyze_categories.take(),
enable,
metrics_explicit,
));
}
"summary" => {
let summary = parse_bool_arg(&opt.arg, &name)?;
out.analyze_level = Some(if summary {
MetricType::Summary
} else {
MetricType::Dev
});
}
"costs" => {
out.show_statistics = Some(parse_bool_arg(&opt.arg, &name)?);
}
"buffers" | "wal" | "settings" | "generic_plan" | "memory" => {
let upper = name.to_ascii_uppercase();
return Err(DataFusionError::NotImplemented(format!(
"EXPLAIN option {upper} is not supported by DataFusion; \
see METRICS for category filtering"
)));
}
_ => {
return Err(DataFusionError::Plan(format!(
"unknown EXPLAIN option: {}",
opt.name.value
)));
}
}
}
Ok(out)
}
}
#[cfg(feature = "sql")]
fn parse_bool_arg(arg: &Option<Expr>, name: &str) -> Result<bool> {
let Some(expr) = arg else {
return Ok(true);
};
match expr {
Expr::Identifier(ident) => match ident.value.to_ascii_lowercase().as_str() {
"true" | "on" => Ok(true),
"false" | "off" => Ok(false),
other => Err(DataFusionError::Plan(format!(
"expected boolean for EXPLAIN option {name}, got '{other}'"
))),
},
Expr::Value(ValueWithSpan { value, .. }) => match value {
Value::Boolean(b) => Ok(*b),
Value::Number(n, _) => match n.as_str() {
"0" => Ok(false),
"1" => Ok(true),
other => Err(DataFusionError::Plan(format!(
"expected boolean (0 or 1) for EXPLAIN option {name}, got '{other}'"
))),
},
Value::SingleQuotedString(s) | Value::DoubleQuotedString(s) => {
match s.to_ascii_lowercase().as_str() {
"true" | "on" | "1" => Ok(true),
"false" | "off" | "0" => Ok(false),
other => Err(DataFusionError::Plan(format!(
"expected boolean for EXPLAIN option {name}, got '{other}'"
))),
}
}
other => Err(DataFusionError::Plan(format!(
"expected boolean for EXPLAIN option {name}, got '{other}'"
))),
},
other => Err(DataFusionError::Plan(format!(
"expected boolean for EXPLAIN option {name}, got '{other}'"
))),
}
}
#[cfg(feature = "sql")]
fn parse_ident_or_string_arg(arg: &Option<Expr>, name: &str) -> Result<String> {
let expr = arg.as_ref().ok_or_else(|| {
DataFusionError::Plan(format!(
"EXPLAIN option {} requires an argument",
name.to_ascii_uppercase()
))
})?;
match expr {
Expr::Identifier(ident) => Ok(ident.value.clone()),
Expr::Value(ValueWithSpan { value, .. }) => match value {
Value::SingleQuotedString(s) | Value::DoubleQuotedString(s) => Ok(s.clone()),
other => Err(DataFusionError::Plan(format!(
"expected identifier or string for EXPLAIN option {name}, got '{other}'"
))),
},
other => Err(DataFusionError::Plan(format!(
"expected identifier or string for EXPLAIN option {name}, got '{other}'"
))),
}
}
#[cfg(feature = "sql")]
fn adjust_timing(
current: Option<ExplainAnalyzeCategories>,
enable: bool,
metrics_explicit: bool,
) -> ExplainAnalyzeCategories {
if !metrics_explicit {
return if enable {
ExplainAnalyzeCategories::All
} else {
ExplainAnalyzeCategories::Only(vec![
MetricCategory::Rows,
MetricCategory::Bytes,
MetricCategory::Uncategorized,
])
};
}
match current.unwrap_or(ExplainAnalyzeCategories::All) {
ExplainAnalyzeCategories::All if enable => ExplainAnalyzeCategories::All,
ExplainAnalyzeCategories::All => {
ExplainAnalyzeCategories::Only(vec![
MetricCategory::Rows,
MetricCategory::Bytes,
MetricCategory::Uncategorized,
])
}
ExplainAnalyzeCategories::Only(mut cats) if enable => {
if !cats.contains(&MetricCategory::Timing) {
cats.push(MetricCategory::Timing);
}
ExplainAnalyzeCategories::Only(cats)
}
ExplainAnalyzeCategories::Only(cats) => ExplainAnalyzeCategories::Only(
cats.into_iter()
.filter(|c| *c != MetricCategory::Timing)
.collect(),
),
}
}
#[cfg(all(test, feature = "sql"))]
mod explain_options_tests {
use super::*;
use sqlparser::ast::Ident;
use sqlparser::tokenizer::Span;
fn bare(name: &str) -> UtilityOption {
UtilityOption {
name: Ident {
value: name.to_string(),
quote_style: None,
span: Span::empty(),
},
arg: None,
}
}
fn with_ident_arg(name: &str, arg: &str) -> UtilityOption {
UtilityOption {
name: Ident {
value: name.to_string(),
quote_style: None,
span: Span::empty(),
},
arg: Some(Expr::Identifier(Ident {
value: arg.to_string(),
quote_style: None,
span: Span::empty(),
})),
}
}
fn with_string_arg(name: &str, arg: &str) -> UtilityOption {
UtilityOption {
name: Ident {
value: name.to_string(),
quote_style: None,
span: Span::empty(),
},
arg: Some(Expr::Value(ValueWithSpan {
value: Value::SingleQuotedString(arg.to_string()),
span: Span::empty(),
})),
}
}
fn with_bool_arg(name: &str, b: bool) -> UtilityOption {
UtilityOption {
name: Ident {
value: name.to_string(),
quote_style: None,
span: Span::empty(),
},
arg: Some(Expr::Value(ValueWithSpan {
value: Value::Boolean(b),
span: Span::empty(),
})),
}
}
fn with_number_arg(name: &str, n: &str) -> UtilityOption {
UtilityOption {
name: Ident {
value: name.to_string(),
quote_style: None,
span: Span::empty(),
},
arg: Some(Expr::Value(ValueWithSpan {
value: Value::Number(n.to_string(), false),
span: Span::empty(),
})),
}
}
#[test]
fn bare_analyze_and_verbose() {
let opts = ExplainStatementOptions::from_utility_options(&[
bare("ANALYZE"),
bare("VERBOSE"),
])
.unwrap();
assert!(opts.analyze);
assert!(opts.verbose);
assert!(opts.format.is_none());
}
#[test]
fn format_from_ident_and_string() {
let opts = ExplainStatementOptions::from_utility_options(&[with_ident_arg(
"FORMAT", "tree",
)])
.unwrap();
assert_eq!(opts.format, Some(ExplainFormat::Tree));
let opts = ExplainStatementOptions::from_utility_options(&[with_string_arg(
"FORMAT", "pgjson",
)])
.unwrap();
assert_eq!(opts.format, Some(ExplainFormat::PostgresJSON));
}
#[test]
fn metrics_and_level() {
let opts = ExplainStatementOptions::from_utility_options(&[
with_string_arg("METRICS", "rows,bytes"),
with_ident_arg("LEVEL", "dev"),
])
.unwrap();
assert_eq!(
opts.analyze_categories,
Some(ExplainAnalyzeCategories::Only(vec![
MetricCategory::Rows,
MetricCategory::Bytes,
]))
);
assert_eq!(opts.analyze_level, Some(MetricType::Dev));
}
#[test]
fn on_off_numeric_bool() {
let opts = ExplainStatementOptions::from_utility_options(&[
with_ident_arg("ANALYZE", "ON"),
with_ident_arg("VERBOSE", "off"),
with_bool_arg("COSTS", true),
])
.unwrap();
assert!(opts.analyze);
assert!(!opts.verbose);
assert_eq!(opts.show_statistics, Some(true));
let opts = ExplainStatementOptions::from_utility_options(&[
with_number_arg("ANALYZE", "1"),
with_number_arg("VERBOSE", "0"),
])
.unwrap();
assert!(opts.analyze);
assert!(!opts.verbose);
}
#[test]
fn summary_sugar_sets_level() {
let opts = ExplainStatementOptions::from_utility_options(&[with_ident_arg(
"SUMMARY", "ON",
)])
.unwrap();
assert_eq!(opts.analyze_level, Some(MetricType::Summary));
let opts = ExplainStatementOptions::from_utility_options(&[with_bool_arg(
"SUMMARY", false,
)])
.unwrap();
assert_eq!(opts.analyze_level, Some(MetricType::Dev));
}
#[test]
fn timing_merges_with_metrics() {
let opts = ExplainStatementOptions::from_utility_options(&[
with_string_arg("METRICS", "rows,timing"),
with_bool_arg("TIMING", false),
])
.unwrap();
assert_eq!(
opts.analyze_categories,
Some(ExplainAnalyzeCategories::Only(vec![MetricCategory::Rows]))
);
let opts = ExplainStatementOptions::from_utility_options(&[
with_string_arg("METRICS", "rows"),
with_bool_arg("TIMING", true),
])
.unwrap();
assert_eq!(
opts.analyze_categories,
Some(ExplainAnalyzeCategories::Only(vec![
MetricCategory::Rows,
MetricCategory::Timing,
]))
);
}
#[test]
fn timing_alone() {
let opts = ExplainStatementOptions::from_utility_options(&[with_bool_arg(
"TIMING", false,
)])
.unwrap();
assert_eq!(
opts.analyze_categories,
Some(ExplainAnalyzeCategories::Only(vec![
MetricCategory::Rows,
MetricCategory::Bytes,
MetricCategory::Uncategorized,
]))
);
}
#[test]
fn unknown_option_rejected() {
let err =
ExplainStatementOptions::from_utility_options(&[bare("FOO")]).unwrap_err();
assert!(
err.to_string().contains("unknown EXPLAIN option: FOO"),
"got: {err}"
);
}
#[test]
fn postgres_only_options_rejected() {
for pg_only in ["BUFFERS", "WAL", "SETTINGS", "GENERIC_PLAN", "MEMORY"] {
let err = ExplainStatementOptions::from_utility_options(&[bare(pg_only)])
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(pg_only),
"msg did not include {pg_only}: {msg}"
);
assert!(msg.contains("not supported"), "msg: {msg}");
}
}
}