use std::sync::Mutex;
use serde::Serialize;
use super::report::{Finding, FindingKind, Report};
pub const DEFAULT_THRESHOLD: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum QueryEvent {
Parent {
relation: String,
query: String,
row_count: usize,
},
Child { relation: String, query: String },
}
pub struct Tracker {
events: Mutex<Vec<QueryEvent>>,
threshold: usize,
}
impl Tracker {
#[must_use]
pub fn new() -> Self {
Self::with_threshold(DEFAULT_THRESHOLD)
}
#[must_use]
pub fn with_threshold(threshold: usize) -> Self {
Self {
events: Mutex::new(Vec::new()),
threshold,
}
}
pub fn record_parent_load(&self, relation: &str, query: &str, row_count: usize) {
if let Ok(mut events) = self.events.lock() {
events.push(QueryEvent::Parent {
relation: relation.to_owned(),
query: query.to_owned(),
row_count,
});
}
}
pub fn record_child_query(&self, relation: &str, query: &str) {
if let Ok(mut events) = self.events.lock() {
events.push(QueryEvent::Child {
relation: relation.to_owned(),
query: query.to_owned(),
});
}
}
#[must_use]
pub fn event_count(&self) -> usize {
self.events.lock().map(|events| events.len()).unwrap_or(0)
}
#[must_use]
pub fn report(&self) -> Report {
let Ok(events) = self.events.lock() else {
return Report::default();
};
use std::collections::HashMap;
#[derive(Default)]
struct Acc {
parent_seen: bool,
parent_query: String,
parent_rows: usize,
child_queries: usize,
}
let mut by_relation: HashMap<String, Acc> = HashMap::new();
for event in events.iter() {
match event {
QueryEvent::Parent {
relation,
query,
row_count,
} => {
let acc = by_relation.entry(relation.clone()).or_default();
acc.parent_seen = true;
acc.parent_query = query.clone();
acc.parent_rows = *row_count;
acc.child_queries = 0;
}
QueryEvent::Child { relation, query } => {
let acc = by_relation.entry(relation.clone()).or_default();
acc.child_queries += 1;
if acc.child_queries == 1 {
let _ = query;
}
}
}
}
let mut findings = Vec::new();
for (relation, acc) in by_relation {
if acc.parent_seen && acc.parent_rows > 1 && acc.child_queries >= self.threshold {
findings.push(Finding {
relation: relation.clone(),
kind: FindingKind::RelationPerRow,
parent_query: acc.parent_query,
child_queries: acc.child_queries,
recommendation: format!(
"eager-load `{relation}` instead of per-row access \
(parent loaded {rows} rows, {children} per-row child queries fired)",
rows = acc.parent_rows,
children = acc.child_queries
),
});
}
}
findings.sort_by(|a, b| a.relation.cmp(&b.relation));
Report { findings }
}
}
impl Default for Tracker {
fn default() -> Self {
Self::new()
}
}