use std::fmt::Write as _;
use rudb_common::stat::{Class, Classes, Stat, Use};
use rudb_metrics::{Document, Operator, commas};
use rudb_plan::{
ColumnBinding, Keys, Node, NodeRef, OperatorRef, PipelineRef, Plan, Shape, keys_of, seams_of,
};
use rudb_seam::{Registries, SeamId, Settings};
use crate::estimate::{CARDINALITY, DISTINCT, Facts, rows_stat, rows_stat_into};
use crate::pass::Context;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Statistics {
Asked,
NotAsked,
}
#[derive(Debug, Clone, Copy)]
pub struct Seams<'a> {
settings: &'a Settings,
registries: &'a Registries,
}
impl<'a> Seams<'a> {
#[must_use]
pub fn new(settings: &'a Settings, registries: &'a Registries) -> Self {
Self { settings, registries }
}
#[must_use]
pub fn settings(&self) -> &'a Settings {
self.settings
}
fn chosen(self, seam: SeamId) -> Option<(String, bool)> {
let running = self.registries.running(seam, self.settings)?;
let how = if running.pinned { "pinned" } else { "default" };
Some((format!("{} ({how})", running.name), running.is_reference))
}
fn all_reference(self, node: &Node) -> bool {
seams_of(node).iter().all(|seam| self.chosen(*seam).is_none_or(|(_, reference)| reference))
}
}
#[must_use]
pub fn explain(plan: &Plan, context: &Context) -> String {
let settings = Settings::new();
let registries = Registries::new();
explain_with(plan, context, Seams::new(&settings, ®istries), Statistics::NotAsked)
}
#[must_use]
pub fn explain_with(
plan: &Plan,
context: &Context,
seams: Seams<'_>,
statistics: Statistics,
) -> String {
printed(plan, context, seams, None, statistics)
}
#[must_use]
pub fn analyzed(
plan: &Plan,
context: &Context,
seams: Seams<'_>,
measured: &Document,
statistics: Statistics,
) -> String {
printed(plan, context, seams, Some(measured), statistics)
}
pub fn record_estimates(plan: &Plan, facts: &Facts, document: &mut Document) {
let shape = Shape::of(plan);
let mut estimated = vec![Stat::Unknown; shape.operators() as usize];
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
if let Some(id) = shape.operator_of(node) {
estimated[id as usize] = rows_stat(plan, node, facts);
}
}
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
let Node::Filter { input, .. } = *plan.node(node) else { continue };
if crate::bounds::into_scan(plan, node).is_none() {
continue;
}
if let Some(id) = shape.operator_of(input) {
estimated[id as usize] = rows_stat(plan, node, facts);
}
}
for operator in &mut document.operators {
if let Some(estimate) = estimated.get(operator.id as usize) {
operator.estimated_rows = estimate.value().copied();
operator.estimate_class = estimate.class();
operator.estimate_provenance = estimate.provenance();
document.estimates.record(estimate);
}
}
}
fn printed(
plan: &Plan,
context: &Context,
seams: Seams<'_>,
measured: Option<&Document>,
statistics: Statistics,
) -> String {
let facts = context.facts();
let shape = Shape::of(plan);
let keys = keys_of(plan);
let printing =
Printing { plan, context, facts, shape: &shape, seams, measured, statistics, keys: &keys };
let mut out = String::new();
printing.write_node(plan.root(), 0, false, &mut out);
write_pipelines(&shape, measured, &mut out);
write_seams(seams, &mut out);
if statistics == Statistics::Asked {
write_statistics(reads(plan, facts, &shape), measured, &mut out);
}
if let Some(measured) = measured {
write_totals(measured, &mut out);
}
out
}
fn estimate(stat: Stat<u64>, statistics: Statistics) -> String {
let read = match statistics {
Statistics::Asked => format!(", read to {CARDINALITY}"),
Statistics::NotAsked => String::new(),
};
match stat {
Stat::Unknown => format!("rows unknown{read}"),
Stat::Known { value, class, provenance } => match class {
Class::Estimated => format!("~{value} rows {class} from {provenance}{read}"),
class => format!("{value} rows {class} from {provenance}{read}"),
},
}
}
#[derive(Clone, Copy)]
struct Printing<'a> {
plan: &'a Plan,
context: &'a Context,
facts: &'a Facts,
shape: &'a Shape,
seams: Seams<'a>,
measured: Option<&'a Document>,
statistics: Statistics,
keys: &'a [Keys],
}
impl Printing<'_> {
fn write_node(self, node: NodeRef, depth: usize, filtered: bool, out: &mut String) {
let printed = self.plan.operator(node);
let estimate = estimate(rows_stat(self.plan, node, self.facts), self.statistics);
let pipeline = self.shape.pipeline(node);
let marker =
if self.seams.all_reference(self.plan.node(node)) { " [reference]" } else { "" };
let moved = self.measured.is_some() && crate::bounds::into_scan(self.plan, node).is_some();
let actual = if moved {
" [applied by the scan below]".to_owned()
} else {
self.measured
.map(|measured| actually(measured, self.shape.operator(node), filtered))
.unwrap_or_default()
};
let told = match self.statistics {
Statistics::Asked => tells_apart(self.keys.get(node as usize)),
Statistics::NotAsked => String::new(),
};
let chose = chosen(self.plan, node, self.context);
let room = sized(self.plan, node);
let _ = writeln!(
out,
"{:indent$}{printed} [{estimate}] [pipeline {pipeline}]{told}{room}{chose}{marker}{actual}",
"",
indent = depth * 2
);
if let (Some(measured), Some(gathered)) = (self.measured, self.shape.gathered(node)) {
if let Some(operator) = row(measured, gathered) {
let _ = writeln!(
out,
"{:indent$}{} of the side that finishes first{}",
"",
operator.kind,
actually(measured, gathered, false),
indent = (depth + 1) * 2
);
}
}
for child in children(self.plan.node(node)) {
self.write_node(child, depth + 1, moved, out);
}
}
}
fn sized(plan: &Plan, node: NodeRef) -> String {
let Node::Aggregate { index, .. } = *plan.node(node) else { return String::new() };
let mut said = Vec::new();
if let Some(groups) = plan.presized(index) {
said.push(format!("room for {} groups", commas(groups)));
}
if let Some((low, values)) = plan.dense(index) {
said.push(format!("addressed directly over {} values from {low}", commas(values)));
}
if plan.clustered(index) {
said.push("groups closed in key order".to_owned());
}
if said.is_empty() { String::new() } else { format!(" [{}]", said.join(", ")) }
}
fn chosen(plan: &Plan, node: NodeRef, context: &Context) -> String {
if context.links().is_empty() {
return String::new();
}
let Some(why) = crate::link::why(plan, node, context) else {
return String::new();
};
let taken = matches!(plan.node(node), Node::LinkJoin { .. });
match (taken, why.chosen()) {
(true, _) => format!(" [reads the link, because {why}]"),
(false, false) => format!(" [builds a hash table, because {why}]"),
(false, true) => " [builds a hash table, because the link_join rewrite is off]".to_owned(),
}
}
fn actually(measured: &Document, id: OperatorRef, filtered: bool) -> String {
let Some(operator) = row(measured, id) else {
return " [not measured]".to_owned();
};
let held = operator.memory.high_water;
let memory = if held == 0 { String::new() } else { format!(", {} held", bytes(held)) };
let slow = match operator.fallbacks.worst() {
None => String::new(),
Some((cause, _)) => {
format!(", {} fell back, most of it {}", operator.fallbacks.total(), cause.name())
}
};
let after = if filtered { " after the filter above" } else { "" };
let joined = match &operator.joined {
None => String::new(),
Some(joined) => {
format!(", {} over {} build rows", joined.algorithm.name(), joined.build_rows)
}
};
let parts = operator.parts_read.saturating_add(operator.parts_pruned);
let skipped = if operator.parts_pruned == 0 {
String::new()
} else {
format!(", {} of {parts} parts skipped", operator.parts_pruned)
};
let reduced = match &operator.reduced {
None => String::new(),
Some(reduced) if reduced.stopped => {
", link reduction stopped after a third of the rows removed nothing".to_owned()
}
Some(reduced) if reduced.by_key => {
format!(", key map kept {} of {} parent keys", reduced.kept, reduced.rows)
}
Some(reduced) => format!(", link kept {} of {} rows", reduced.kept, reduced.rows),
};
let spent = if operator.cpu_ns == 0 {
duration(operator.wall_ns) + " wall"
} else {
format!("{} wall, {} cpu", duration(operator.wall_ns), duration(operator.cpu_ns))
};
format!(
" [{} rows{after}, {spent}{joined}{skipped}{reduced}{memory}{slow}]",
operator.rows_out
)
}
fn row(measured: &Document, id: OperatorRef) -> Option<&Operator> {
measured.operators.iter().find(|operator| operator.id == id)
}
fn write_pipelines(shape: &Shape, measured: Option<&Document>, out: &mut String) {
let _ = writeln!(out, "\nPipelines");
for pipeline in shape.all() {
let waits = shape.waits_for(pipeline);
let waiting = if waits.is_empty() {
"waits for nothing".to_owned()
} else {
format!("waits for {}", listed(waits))
};
let root = if pipeline == ROOT { ", and the answer comes out of it" } else { "" };
let took = measured
.and_then(|measured| measured.pipelines.iter().find(|row| row.id == pipeline))
.map(|row| format!(" [{} wall, {} cpu]", duration(row.wall_ns), duration(row.cpu_ns)))
.unwrap_or_default();
let _ = writeln!(out, " pipeline {pipeline} {waiting}{root}{took}");
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Reads {
answer: Classes,
enable: Classes,
decide: Classes,
}
impl Reads {
fn record(&mut self, use_: Use, stat: &Stat<u64>) {
match use_ {
Use::Answer => self.answer.record(stat),
Use::Enable => self.enable.record(stat),
Use::Decide => self.decide.record(stat),
}
}
const fn of(self, use_: Use) -> Classes {
match use_ {
Use::Answer => self.answer,
Use::Enable => self.enable,
Use::Decide => self.decide,
}
}
}
fn reads(plan: &Plan, facts: &Facts, shape: &Shape) -> Reads {
let mut reads = Reads::default();
let mut distincts = Vec::new();
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
if shape.operator_of(node).is_some() {
distincts.clear();
let rows = rows_stat_into(plan, node, facts, &mut distincts);
reads.record(CARDINALITY, &rows);
for distinct in &distincts {
reads.record(DISTINCT, distinct);
}
}
}
reads
}
fn tells_apart(keys: Option<&Keys>) -> String {
let Some(keys) = keys else { return String::new() };
let mut said = Vec::new();
if keys.at_most_one_row() {
said.push("at most one row".to_owned());
} else {
let sets: Vec<String> = keys
.sets()
.iter()
.map(|set| set.iter().map(column_name).collect::<Vec<_>>().join(" "))
.collect();
if !sets.is_empty() {
said.push(format!("key {}", among(&sets, "or")));
} else if keys.row() {
said.push("key the whole row".to_owned());
}
}
let fixed: Vec<String> = keys.constants().map(|column| column_name(&column)).collect();
if !fixed.is_empty() {
said.push(format!("{} fixed", among(&fixed, "and")));
}
if said.is_empty() { String::new() } else { format!(" [{}]", said.join(", ")) }
}
fn column_name(column: &ColumnBinding) -> String {
format!("#{}.{}", column.table, column.column)
}
fn write_statistics(reads: Reads, measured: Option<&Document>, out: &mut String) {
let _ = writeln!(out, "\nStatistics");
let mut silent = Vec::new();
for use_ in [Use::Answer, Use::Enable, Use::Decide] {
let classes = reads.of(use_);
if classes.total() == 0 {
silent.push(format!("to {}", use_.name()));
continue;
}
let share = classes.known_share() * 100.0;
let _ = writeln!(
out,
" {} read to {use_}: {classes}, {share:.0}% of them with a number behind them",
classes.total()
);
}
if !silent.is_empty() {
let _ = writeln!(out, " nothing was read {}", among(&silent, "or"));
}
write_q_errors(measured, out);
}
fn write_q_errors(measured: Option<&Document>, out: &mut String) {
let Some(measured) = measured else { return };
let errors = measured.q_errors();
if errors.total() == 0 {
return;
}
let _ = writeln!(
out,
" q-error against the rows the run produced, {} measured",
commas(errors.total())
);
for (class, spread) in errors.named() {
if spread.total() == 0 {
continue;
}
let _ = writeln!(out, " {class} {}: {spread}", commas(spread.total()));
}
}
fn write_totals(measured: &Document, out: &mut String) {
let timing = &measured.timing;
let _ = writeln!(out, "\nTotals");
let planning =
timing.parse_ns.saturating_add(timing.bind_ns).saturating_add(timing.optimize_ns);
let _ = writeln!(
out,
" {} planning, {} building the tree, {} running it, {} in all",
duration(planning),
duration(timing.physical_ns),
duration(timing.execute_ns),
duration(timing.total_ns)
);
let _ = writeln!(
out,
" {} of cpu, {} held at the peak",
duration(measured.resource.cpu_ns),
bytes(measured.resource.peak_bytes)
);
let warnings = measured.warnings();
if !warnings.is_empty() {
let _ = writeln!(out, "\nWarnings");
for warning in &warnings {
let _ = writeln!(out, " {warning}");
}
}
}
fn duration(ns: u64) -> String {
match ns {
0 => "0s".to_owned(),
1..1_000 => format!("{ns}ns"),
1_000..1_000_000 => format!("{:.3}us", ns as f64 / 1_000.0),
1_000_000..1_000_000_000 => format!("{:.3}ms", ns as f64 / 1_000_000.0),
_ => format!("{:.3}s", ns as f64 / 1_000_000_000.0),
}
}
fn bytes(count: u64) -> String {
match count {
0..1024 => format!("{count} bytes"),
1024..1_048_576 => format!("{:.1} KiB", count as f64 / 1024.0),
1_048_576..1_073_741_824 => format!("{:.1} MiB", count as f64 / 1_048_576.0),
_ => format!("{:.1} GiB", count as f64 / 1_073_741_824.0),
}
}
fn write_seams(seams: Seams<'_>, out: &mut String) {
let _ = writeln!(out, "\nSeams");
let mut printed = 0;
for seam in SeamId::ALL {
if let Some((chosen, _)) = seams.chosen(*seam) {
let _ = writeln!(out, " {} = {chosen}", seam.name());
printed += 1;
}
}
let unregistered = SeamId::ALL.len() - printed;
if unregistered > 0 {
let _ = writeln!(
out,
" {unregistered} seams have nothing registered and are running their reference implementation, see rudb_strategies()"
);
}
}
const ROOT: PipelineRef = 0;
fn listed(pipelines: &[PipelineRef]) -> String {
among(&pipelines.iter().map(u32::to_string).collect::<Vec<String>>(), "and")
}
fn among(words: &[String], conjunction: &str) -> String {
match words.split_last() {
None => String::new(),
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} {conjunction} {last}", rest.join(", ")),
}
}
fn children(node: &Node) -> Vec<NodeRef> {
node.children().into_iter().flatten().collect()
}
#[cfg(test)]
mod tests {
use rudb_common::stat::Class;
use rudb_metrics::{Document, Operator};
use rudb_plan::Plan;
use rudb_seam::{Registries, Settings};
use super::{Context, Seams, Shape, Statistics, explain, explain_with, record_estimates};
use crate::estimate::Facts;
fn parsed(text: &str) -> Plan {
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"))
}
fn knowing(tables: &[(&str, u64)]) -> Context {
let mut facts = Facts::new();
for (table, count) in tables {
facts.record("memory", "main", table, *count);
}
let mut context = Context::new();
context.measure(std::sync::Arc::new(facts));
context
}
fn printed(text: &str, tables: &[(&str, u64)]) -> String {
explain(&parsed(text), &knowing(tables))
}
fn asked(text: &str, tables: &[(&str, u64)]) -> String {
let settings = Settings::new();
let registries = Registries::new();
explain_with(
&parsed(text),
&knowing(tables),
Seams::new(&settings, ®istries),
Statistics::Asked,
)
}
fn tree(out: &str) -> Vec<&str> {
out.lines().take_while(|line| !line.is_empty()).collect()
}
#[test]
fn every_operator_gets_a_line_with_its_own_estimate_on_it() {
let out = printed(
concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
),
&[("t", 1000)],
);
assert_eq!(
tree(&out).join("\n"),
concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN [~200 rows estimated from default] \
[pipeline 0] [reference]\n",
" Get memory.main.t AS t #0 [a::INTEGER] [1000 rows exact from row count] [pipeline 0] \
[reference]",
)
);
}
#[test]
fn a_plain_explain_says_nothing_about_uses_and_asking_for_the_statistics_says_it_on_every_line()
{
let plan = concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
);
let quiet = printed(plan, &[("t", 1000)]);
assert!(!quiet.contains("read to"), "{quiet}");
assert!(!quiet.contains("\nStatistics\n"), "{quiet}");
let out = asked(plan, &[("t", 1000)]);
for line in tree(&out) {
assert!(line.contains(", read to decide]"), "{line}");
}
}
fn aggregate(room: Option<u64>, array: Option<(i128, u64)>) -> Plan {
let text = concat!(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
);
let mut plan = parsed(text);
if let Some(groups) = room {
plan.presize(1, groups);
}
if let Some((low, values)) = array {
plan.densify(1, low, values);
}
plan
}
#[test]
fn an_aggregate_says_how_much_room_it_asked_for_and_how_it_finds_a_group() {
let out =
explain(&aggregate(Some(120_000), Some((100, 1_000_000))), &knowing(&[("t", 10)]));
let first = tree(&out)[0];
assert!(first.contains("[room for 120,000 groups, addressed directly"), "{first}");
assert!(first.contains("over 1,000,000 values from 100]"), "{first}");
}
#[test]
fn an_aggregate_no_pass_wrote_a_number_for_says_nothing_extra_at_all() {
let out = explain(&aggregate(None, None), &knowing(&[("t", 10)]));
let first = tree(&out)[0];
assert!(!first.contains("room for"), "{first}");
assert!(!first.contains("addressed"), "{first}");
}
#[test]
fn the_statistics_section_counts_the_classes_and_says_which_uses_never_happened() {
let out = asked(
concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
),
&[("t", 1000)],
);
assert!(out.contains("\nStatistics\n"), "{out}");
assert!(
out.contains(
" 2 read to decide: exact 1, certified 0, estimated 1, unknown 0, \
100% of them with a number behind them\n"
),
"{out}"
);
assert!(out.contains(" nothing was read to answer or to enable\n"), "{out}");
}
#[test]
fn a_plan_nobody_measured_says_so_in_the_section_as_well_as_on_the_lines() {
let out = asked("Get memory.main.t AS t #0 [a::INTEGER]\n", &[]);
assert!(out.contains("[rows unknown, read to decide]"), "{out}");
assert!(
out.contains(
" 1 read to decide: exact 0, certified 0, estimated 0, unknown 1, \
0% of them with a number behind them\n"
),
"{out}"
);
}
#[test]
fn an_operator_nobody_can_estimate_says_so_rather_than_saying_nothing() {
let out = printed("Get memory.main.t AS t #0 [a::INTEGER]\n", &[]);
assert!(tree(&out)[0].contains("[rows unknown]"), "{out}");
}
#[test]
fn the_document_gets_one_class_per_operator_and_the_number_that_goes_with_it() {
let plan = parsed(concat!(
"Aggregate #1 groups=[#0.0::INTEGER] aggregates=[count_star()::BIGINT]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
));
let mut facts = Facts::new();
facts.record("memory", "main", "t", 1000);
let mut document = Document::new("select");
let shape = Shape::of(&plan);
for id in 0..shape.operators() {
document.operators.push(Operator::new(id, 0, "operator"));
}
record_estimates(&plan, &facts, &mut document);
assert_eq!(document.estimates.total(), 2);
assert_eq!(document.estimates.exact(), 1);
assert_eq!(document.estimates.estimated(), 1);
assert_eq!(document.estimates.unknown(), 0);
for operator in &document.operators {
assert_eq!(operator.estimated_rows.is_some(), operator.estimate_class.is_some());
}
}
#[test]
fn a_filter_the_scan_applies_is_the_estimate_on_the_scans_row() {
let plan = parsed(concat!(
"Filter (#0.0::INTEGER > 1::INTEGER)::BOOLEAN\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
));
let mut facts = Facts::new();
facts.record("memory", "main", "t", 1000);
let mut document = Document::new("select");
let shape = Shape::of(&plan);
let scan = shape.operator_of(1).expect("the scan is an operator");
document.operators.push(Operator::new(scan, 0, "Scan"));
record_estimates(&plan, &facts, &mut document);
assert_eq!(document.operators[0].estimated_rows, Some(200));
assert_eq!(document.operators[0].estimate_class, Some(Class::Estimated));
}
#[test]
fn an_operator_nobody_estimated_is_counted_as_nobody_knowing_rather_than_left_out() {
let plan = parsed("TableFunction range args=[] #0 [a::BIGINT]\n");
let mut document = Document::new("select");
let shape = Shape::of(&plan);
for id in 0..shape.operators() {
document.operators.push(Operator::new(id, 0, "operator"));
}
record_estimates(&plan, &Facts::new(), &mut document);
assert_eq!(document.estimates.unknown(), document.estimates.total());
assert!(document.estimates.total() > 0);
assert_eq!(document.operators[0].estimated_rows, None);
assert_eq!(document.operators[0].estimate_class, None);
}
#[test]
fn both_sides_of_a_join_are_printed_under_it_and_each_carries_its_own_number() {
let out = printed(
concat!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.small AS small #0 [a::INTEGER]\n",
" Get memory.main.big AS big #1 [a::INTEGER]\n",
),
&[("small", 10), ("big", 5000)],
);
let lines = tree(&out);
assert_eq!(lines.len(), 3, "{out}");
assert!(lines[0].contains("[~5000 rows estimated from default]"), "{out}");
assert!(
lines[1].contains("small") && lines[1].contains("[10 rows exact from row count]"),
"{out}"
);
assert!(
lines[2].contains("big") && lines[2].contains("[5000 rows exact from row count]"),
"{out}"
);
assert!(lines[1].starts_with(" Get"), "{out}");
}
#[test]
fn a_session_with_no_relationships_says_nothing_about_links_on_any_join() {
let out = printed(
concat!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.l AS l #0 [a::INTEGER]\n",
" Get memory.main.r AS r #1 [a::INTEGER]\n",
),
&[("l", 10), ("r", 10)],
);
assert!(!out.contains("hash table"), "{out}");
assert!(!out.contains("reads the link"), "{out}");
}
#[test]
fn a_join_in_a_session_that_has_relationships_says_which_algorithm_and_why() {
let mut context = knowing(&[("lineitem", 6_000_000), ("orders", 1_500_000)]);
context.relate(std::sync::Arc::new(vec![crate::link::Linked::built(
"lineitem",
"l_orderkey",
"orders",
"o_orderkey",
)]));
let mut plan = parsed(concat!(
"Project #2 [#0.0::BIGINT AS k]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.lineitem AS lineitem #0 [l_orderkey::BIGINT]\n",
" Get memory.main.orders AS orders #1 [o_orderkey::BIGINT]\n",
));
let out = explain(&plan, &context);
assert!(
out.contains("[builds a hash table, because the link_join rewrite is off]"),
"{out}"
);
assert_eq!(
tree(&out).iter().filter(|line| line.contains("hash table")).count(),
1,
"{out}"
);
crate::pass::Pass::run(&crate::link::LinkJoinRewrite, &mut plan, &context)
.expect("the pass does not fail");
let out = explain(&plan, &context);
assert!(
out.contains(
"[reads the link, because the parent does not fit in cache and its projection \
is 8 bytes, which is under 32]"
),
"{out}"
);
}
#[test]
fn a_plan_that_does_not_break_is_one_pipeline_and_says_so() {
let out = printed("Get memory.main.t AS t #0 [a::INTEGER]\n", &[("t", 4)]);
assert!(out.contains("[pipeline 0]"), "{out}");
assert!(
out.contains(" pipeline 0 waits for nothing, and the answer comes out of it"),
"{out}"
);
}
#[test]
fn a_sort_prints_the_pipeline_it_ends_and_the_edge_above_it() {
let out = printed(
concat!(
"Sort [#0.0::INTEGER ASC NULLS LAST]\n",
" Get memory.main.t AS t #0 [a::INTEGER]\n",
),
&[("t", 100)],
);
let lines = tree(&out);
assert!(lines[0].contains("[pipeline 1]"), "the sort ends the one below it: {out}");
assert!(lines[1].contains("[pipeline 1]"), "{out}");
assert!(out.contains(" pipeline 0 waits for 1"), "{out}");
assert!(out.contains(" pipeline 1 waits for nothing"), "{out}");
}
#[test]
fn a_join_prints_three_pipelines_in_the_order_they_have_to_run() {
let out = printed(
concat!(
"Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n",
" Get memory.main.l AS l #0 [a::INTEGER]\n",
" Get memory.main.r AS r #1 [a::INTEGER]\n",
),
&[("l", 10), ("r", 10)],
);
let lines = tree(&out);
assert!(lines[0].contains("[pipeline 2]"), "{out}");
assert!(lines[1].contains("[pipeline 2]"), "the probing side: {out}");
assert!(lines[2].contains("[pipeline 1]"), "the gathered side runs first: {out}");
assert!(out.contains(" pipeline 0 waits for 2"), "{out}");
assert!(out.contains(" pipeline 2 waits for 1"), "{out}");
}
#[test]
fn with_nothing_registered_the_seam_section_says_what_that_means() {
let out = printed("Get memory.main.t AS t #0 [a::INTEGER]\n", &[("t", 4)]);
assert!(out.contains("\nSeams\n"), "{out}");
assert!(
out.contains(
" 27 seams have nothing registered and are running their reference implementation"
),
"{out}"
);
}
#[test]
fn a_hint_that_pins_a_seam_nobody_has_registered_changes_nothing_that_prints() {
let mut settings = Settings::new();
settings.pin(rudb_seam::SeamId::Sort, "merge");
let registries = Registries::new();
let plan = parsed(
"Sort [#0.0::INTEGER ASC NULLS LAST]\n Get memory.main.t AS t #0 [a::INTEGER]\n",
);
let out = explain_with(
&plan,
&knowing(&[]),
Seams::new(&settings, ®istries),
Statistics::NotAsked,
);
assert!(out.contains("[reference]"), "{out}");
assert!(!out.contains("sort = "), "{out}");
}
}