mod performance;
pub mod schema_aware;
mod security;
mod style;
mod types;
use rayon::prelude::*;
pub use types::{AnalysisReport, RuleCategory, RuleInfo, Severity, Violation};
use crate::{config::RulesConfig, query::Query, schema::Schema};
pub trait Rule: Send + Sync {
fn info(&self) -> RuleInfo;
fn check(&self, query: &Query, query_index: usize) -> Vec<Violation>;
}
pub struct RuleRunner {
rules: Vec<Box<dyn Rule>>,
severity_cache: std::collections::HashMap<&'static str, Severity>
}
impl Default for RuleRunner {
fn default() -> Self {
Self::new()
}
}
impl RuleRunner {
pub fn new() -> Self {
Self::with_config(RulesConfig::default())
}
pub fn with_config(config: RulesConfig) -> Self {
let all_rules: Vec<Box<dyn Rule>> = vec![
Box::new(performance::SelectStarWithoutLimit),
Box::new(performance::LeadingWildcard),
Box::new(performance::OrInsteadOfIn),
Box::new(performance::LargeOffset),
Box::new(performance::MissingJoinCondition),
Box::new(performance::DistinctWithOrderBy),
Box::new(performance::ScalarSubquery),
Box::new(performance::FunctionOnColumn),
Box::new(performance::NotInWithSubquery),
Box::new(performance::UnionWithoutAll),
Box::new(performance::SelectWithoutWhere),
Box::new(style::SelectStar),
Box::new(style::MissingTableAlias),
Box::new(security::MissingWhereInUpdate),
Box::new(security::MissingWhereInDelete),
Box::new(security::TruncateDetected),
Box::new(security::DropDetected),
];
let rules: Vec<Box<dyn Rule>> = all_rules
.into_iter()
.filter(|r| {
!config
.disabled
.iter()
.any(|d| d.eq_ignore_ascii_case(r.info().id))
})
.collect();
let mut severity_cache = std::collections::HashMap::new();
for rule in &rules {
let rule_id = rule.info().id;
if let Some(sev_str) = config.severity.get(rule_id)
&& let Some(sev) = parse_severity(sev_str)
{
severity_cache.insert(rule_id, sev);
}
}
Self {
rules,
severity_cache
}
}
pub fn with_schema_and_config(schema: Schema, config: RulesConfig) -> Self {
let mut runner = Self::with_config(config.clone());
let schema_rules: Vec<Box<dyn Rule>> = vec![
Box::new(schema_aware::MissingIndexOnFilterColumn::new(
schema.clone()
)),
Box::new(schema_aware::ColumnNotInSchema::new(schema.clone())),
Box::new(schema_aware::SuggestIndex::new(schema)),
];
for rule in schema_rules {
if !config
.disabled
.iter()
.any(|d| d.eq_ignore_ascii_case(rule.info().id))
{
let rule_id = rule.info().id;
if let Some(sev_str) = config.severity.get(rule_id)
&& let Some(sev) = parse_severity(sev_str)
{
runner.severity_cache.insert(rule_id, sev);
}
runner.rules.push(rule);
}
}
runner
}
pub fn analyze(&self, queries: &[Query]) -> AnalysisReport {
let mut report = AnalysisReport::new(queries.len(), self.rules.len());
let violations: Vec<Violation> = queries
.par_iter()
.enumerate()
.flat_map(|(idx, query)| {
self.rules
.par_iter()
.flat_map(|rule| rule.check(query, idx))
.collect::<Vec<_>>()
})
.collect();
for mut violation in violations {
if let Some(&severity) = self.severity_cache.get(violation.rule_id) {
violation.severity = severity;
}
report.add_violation(violation);
}
report.violations.sort_by(|a, b| {
b.severity
.cmp(&a.severity)
.then_with(|| a.query_index.cmp(&b.query_index))
});
report
}
}
fn parse_severity(s: &str) -> Option<Severity> {
match s.to_lowercase().as_str() {
"error" => Some(Severity::Error),
"warning" | "warn" => Some(Severity::Warning),
"info" => Some(Severity::Info),
_ => None
}
}