use rudb::Database;
use rudb_common::{LogicalType, Value};
fn tree(text: &str) -> Vec<&str> {
text.lines().take_while(|line| !line.is_empty()).collect()
}
fn explained(database: &Database, sql: &str) -> String {
let result = database.query(sql).expect("the explain ran");
assert_eq!(result.names(), ["explain_key", "explain_value"], "the shape DuckDB clients expect");
assert_eq!(result.types(), [LogicalType::Varchar, LogicalType::Varchar]);
assert_eq!(result.len(), 1, "the whole tree is one value rather than one row per operator");
match result.value_at(0, 1) {
Value::Varchar(text) => text,
other => panic!("the plan came back as {other:?}"),
}
}
fn with_rows(count: usize) -> Database {
let database = Database::new();
database.execute("CREATE TABLE t (a INTEGER, b VARCHAR)").expect("creates");
database
.execute(&format!("INSERT INTO t SELECT r::INTEGER, 'x' FROM range({count}) AS s(r)"))
.expect("inserts");
database
}
#[test]
fn explain_answers_with_the_plan_and_an_estimate_on_every_line() {
let database = with_rows(1000);
let text = explained(&database, "EXPLAIN SELECT a FROM t WHERE a > 5");
assert!(text.contains("Get memory.main.t"), "{text}");
assert!(text.contains("[~1000 rows]"), "{text}");
assert!(text.contains("[~200 rows]"), "{text}");
for line in tree(&text) {
assert!(line.contains(" rows]"), "a line with no estimate on it: {line}");
}
}
#[test]
fn the_plan_explain_shows_is_the_plan_that_would_have_run() {
let database = with_rows(100);
let text = explained(&database, "EXPLAIN SELECT a FROM t WHERE a > 5");
let ran = database.plan("SELECT a FROM t WHERE a > 5").expect("plans");
let bare: Vec<&str> = tree(&text)
.into_iter()
.map(|line| line.rsplit_once(" [").map_or(line, |(head, _)| head))
.collect();
assert_eq!(bare.join("\n"), ran.trim_end(), "{text}\n{ran}");
}
#[test]
fn an_estimate_nobody_can_make_says_so_rather_than_saying_zero() {
let database = Database::new();
let text = explained(&database, "EXPLAIN SELECT * FROM range(10)");
assert!(text.contains("rows unknown"), "{text}");
}
#[test]
fn an_ungrouped_count_is_one_row_over_a_table_of_any_size() {
let database = with_rows(5000);
let text = explained(&database, "EXPLAIN SELECT count(*) FROM t");
assert!(text.contains("[~1 rows]"), "{text}");
assert!(text.contains("[~5000 rows]"), "{text}");
}
#[test]
fn explain_prints_the_pipelines_a_plan_breaks_into_and_the_edges_between_them() {
let database = with_rows(100);
let text = explained(&database, "EXPLAIN SELECT a FROM t ORDER BY a");
let lines = tree(&text);
assert!(lines[0].contains("[pipeline 1]"), "{text}");
assert!(text.contains(" pipeline 0 waits for 1"), "{text}");
assert!(text.contains(" pipeline 1 waits for nothing"), "{text}");
}
#[test]
fn explain_marks_every_line_that_is_running_a_reference_implementation() {
let database = with_rows(100);
let text = explained(&database, "EXPLAIN SELECT a FROM t WHERE a > 5");
for line in tree(&text) {
assert!(line.ends_with("[reference]"), "a line with no marker on it: {line}");
}
assert!(text.contains("\nSeams\n"), "{text}");
assert!(text.contains("27 seams have nothing registered"), "{text}");
}
#[test]
fn explain_analyze_is_refused_rather_than_answered_with_a_plan_and_no_timings() {
let database = with_rows(10);
let error = database.query("EXPLAIN ANALYZE SELECT a FROM t").expect_err("not yet");
assert_eq!(error.code().duckdb_name(), "Not implemented Error");
assert!(error.to_string().contains("ANALYZE"), "{error}");
}
#[test]
fn explaining_something_that_is_not_a_query_is_refused() {
let database = with_rows(10);
let insert = database.query("EXPLAIN INSERT INTO t VALUES (1, 'x')").expect_err("refused");
assert!(insert.to_string().contains("InsertStatement"), "{insert}");
let create = database.query("EXPLAIN CREATE TABLE u (a INTEGER)").expect_err("refused");
assert!(create.to_string().contains("CreateStatement"), "{create}");
let options = database.query("EXPLAIN (FORMAT JSON) SELECT a FROM t").expect_err("refused");
assert!(options.to_string().contains("ExplainOptionList"), "{options}");
}