use std::{
fmt::{Display, Formatter},
ops::Not,
};
pub mod renderer;
impl Transaction<'_> {
pub fn check_query_plan(&self, sql: &str) {
let query_plan = self.query_plan(sql).expect("Must be a valid SQL");
assert!(
query_plan.contains_unnecessary_scans().not(),
"Query plan contains unnecessary table scan(s):\n{query_plan}"
);
}
fn query_plan(&self, sql: &str) -> rusqlite::Result<QueryPlan> {
let explain_sql = format!("EXPLAIN QUERY PLAN {sql}");
let explain_stmt = self.prepare(&explain_sql)?;
let query_plan = QueryPlanRenderer::new().render_tree(explain_stmt)?;
Ok(QueryPlan { explain_sql, query_plan })
}
}
struct QueryPlan {
explain_sql: String,
query_plan: String,
}
impl Display for QueryPlan {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}\n\n{}", self.explain_sql, self.query_plan)
}
}
impl QueryPlan {
fn contains_unnecessary_scans(&self) -> bool {
use std::sync::LazyLock;
static RE_IDX_SCAN: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::RegexBuilder::new(
r"SCAN ([A-Za-z0-9_]+) (USING( COVERING)?|VIRTUAL TABLE) INDEX ([A-Za-z0-9_]+)",
)
.case_insensitive(true)
.build()
.expect("Must be a valid regex pattern")
});
let query_plan = RE_IDX_SCAN.replace_all(&self.query_plan, r"SEARCH $1 $2 INDEX $4");
if self.explain_sql.contains("SELECT") && !self.explain_sql.contains("WHERE") {
return false;
}
query_plan.contains(" SCAN ")
}
}