use std::cell::RefCell;
use std::fmt;
thread_local! {
static STACK: RefCell<Vec<QueryTrace>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScanMode {
#[default]
Unknown,
CountMetadata,
CountSurvivors,
ArrowShadow,
NativePageCursor,
MultiRunCursor,
NativePushdown,
Materialized,
DirectDispatch,
}
impl fmt::Display for ScanMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ScanMode::Unknown => "unknown",
ScanMode::CountMetadata => "count-metadata",
ScanMode::CountSurvivors => "count-survivors",
ScanMode::ArrowShadow => "arrow-shadow",
ScanMode::NativePageCursor => "native-page-cursor",
ScanMode::MultiRunCursor => "multi-run-cursor",
ScanMode::NativePushdown => "native-pushdown",
ScanMode::Materialized => "materialized",
ScanMode::DirectDispatch => "direct-dispatch",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JoinMode {
#[default]
None,
FkBitmap,
DataFusionHash,
}
impl fmt::Display for JoinMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
JoinMode::None => "none",
JoinMode::FkBitmap => "fk-bitmap",
JoinMode::DataFusionHash => "datafusion-hash",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexRebuild {
#[default]
None,
AlreadyComplete,
Rebuilt,
}
#[derive(Debug, Default, Clone)]
pub struct QueryTrace {
pub scan_mode: ScanMode,
pub run_count: usize,
pub memtable_rows: usize,
pub mutable_run_rows: usize,
pub overlay_rows: usize,
pub conditions_pushed: usize,
pub conditions_residual: usize,
pub survivor_count: Option<usize>,
pub index_rebuild: IndexRebuild,
pub fast_row_id_map: bool,
pub learned_range_used: bool,
pub result_cache_hit: bool,
pub row_materialized: bool,
pub pages_decoded: usize,
pub pages_skipped: usize,
pub join_mode: JoinMode,
pub planning_nanos: u64,
}
impl QueryTrace {
pub fn capture<F, T>(f: F) -> (T, QueryTrace)
where
F: FnOnce() -> T,
{
Self::push_scope();
let result = f();
let trace = Self::pop_scope();
(result, trace)
}
pub fn push_scope() {
STACK.with(|s| s.borrow_mut().push(QueryTrace::default()));
}
pub fn pop_scope() -> QueryTrace {
STACK.with(|s| s.borrow_mut().pop()).unwrap_or_default()
}
#[inline]
pub fn capturing() -> bool {
STACK.with(|s| !s.borrow().is_empty())
}
#[inline]
pub fn record<F>(f: F)
where
F: FnOnce(&mut QueryTrace),
{
STACK.with(|s| {
if let Some(trace) = s.borrow_mut().last_mut() {
f(trace);
}
});
}
pub fn is_fast(&self) -> bool {
!matches!(self.scan_mode, ScanMode::Materialized | ScanMode::Unknown)
&& self.index_rebuild != IndexRebuild::Rebuilt
&& !self.row_materialized
}
}
impl fmt::Display for QueryTrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.scan_mode)?;
if self.run_count > 0 {
write!(f, " runs={}", self.run_count)?;
}
if self.conditions_pushed > 0 {
write!(f, " pushed={}", self.conditions_pushed)?;
}
if self.conditions_residual > 0 {
write!(f, " residual={}", self.conditions_residual)?;
}
if let Some(n) = self.survivor_count {
write!(f, " survivors={}", n)?;
}
let idx = match self.index_rebuild {
IndexRebuild::None => "",
IndexRebuild::AlreadyComplete => " idx=complete",
IndexRebuild::Rebuilt => " idx=REBUILT",
};
f.write_str(idx)?;
if self.result_cache_hit {
f.write_str(" cache=hit")?;
}
if self.learned_range_used {
f.write_str(" learned-range")?;
}
if self.fast_row_id_map {
f.write_str(" fast-rid")?;
}
if self.row_materialized {
f.write_str(" row-mat")?;
}
if self.overlay_rows > 0 {
write!(f, " overlay={}", self.overlay_rows)?;
}
if self.pages_decoded > 0 {
write!(f, " pages={}", self.pages_decoded)?;
}
if self.pages_skipped > 0 {
write!(f, " skipped={}", self.pages_skipped)?;
}
Ok(())
}
}
impl QueryTrace {
pub fn assert_mode(&self, expected: ScanMode) -> &Self {
assert_eq!(
self.scan_mode, expected,
"expected scan mode {expected:?} but got {:?} ({self})",
self.scan_mode
);
self
}
pub fn assert_no_index_rebuild(&self) -> &Self {
assert_ne!(
self.index_rebuild,
IndexRebuild::Rebuilt,
"expected no index rebuild but indexes were rebuilt ({self})"
);
self
}
pub fn assert_not_materialized(&self) -> &Self {
assert!(
!self.row_materialized,
"expected columnar/cursor path but rows were materialized ({self})"
);
self
}
pub fn assert_cache_hit(&self) -> &Self {
assert!(
self.result_cache_hit,
"expected result cache hit but got miss ({self})"
);
self
}
pub fn assert_fast_row_id_map(&self) -> &Self {
assert!(
self.fast_row_id_map,
"expected fast row-id map but got fallback ({self})"
);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_collects_records() {
QueryTrace::record(|t| {
t.run_count = 999;
});
assert!(!QueryTrace::capturing());
let (result, trace) = QueryTrace::capture(|| {
assert!(QueryTrace::capturing());
QueryTrace::record(|t| {
t.run_count = 3;
t.scan_mode = ScanMode::NativePushdown;
});
42
});
assert_eq!(result, 42);
assert_eq!(trace.run_count, 3);
assert_eq!(trace.scan_mode, ScanMode::NativePushdown);
assert!(!QueryTrace::capturing());
}
#[test]
fn nested_captures_are_independent() {
let (outer, outer_trace) = QueryTrace::capture(|| {
QueryTrace::record(|t| t.run_count = 1);
let (inner, inner_trace) = QueryTrace::capture(|| {
QueryTrace::record(|t| t.run_count = 99);
"inner"
});
assert_eq!(inner, "inner");
assert_eq!(inner_trace.run_count, 99);
QueryTrace::record(|t| t.conditions_pushed = 2);
"outer"
});
assert_eq!(outer, "outer");
assert_eq!(outer_trace.run_count, 1);
assert_eq!(outer_trace.conditions_pushed, 2);
}
#[test]
fn display_summary_is_compact() {
let t = QueryTrace {
scan_mode: ScanMode::NativePushdown,
run_count: 1,
conditions_pushed: 2,
survivor_count: Some(12500),
index_rebuild: IndexRebuild::AlreadyComplete,
fast_row_id_map: true,
..Default::default()
};
let s = format!("{t}");
assert!(s.contains("native-pushdown"));
assert!(s.contains("runs=1"));
assert!(s.contains("pushed=2"));
assert!(s.contains("survivors=12500"));
assert!(s.contains("idx=complete"));
assert!(s.contains("fast-rid"));
}
#[test]
fn is_fast_distinguishes_paths() {
let good = QueryTrace {
scan_mode: ScanMode::NativePageCursor,
index_rebuild: IndexRebuild::AlreadyComplete,
..Default::default()
};
assert!(good.is_fast());
let mut bad = good.clone();
bad.index_rebuild = IndexRebuild::Rebuilt;
assert!(!bad.is_fast());
let mut mat = good.clone();
mat.scan_mode = ScanMode::Materialized;
assert!(!mat.is_fast());
}
#[test]
fn assertion_helpers_chain() {
let t = QueryTrace {
scan_mode: ScanMode::ArrowShadow,
index_rebuild: IndexRebuild::AlreadyComplete,
..Default::default()
};
t.assert_mode(ScanMode::ArrowShadow)
.assert_no_index_rebuild()
.assert_not_materialized();
}
}