use std::collections::{HashMap, HashSet};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct QueryStats {
pub query: String,
pub execution_time: Duration,
pub rows_returned: u64,
pub rows_scanned: u64,
pub index_used: bool,
pub tables: Vec<String>,
}
impl QueryStats {
pub fn new(query: String) -> Self {
Self {
query,
execution_time: Duration::from_millis(0),
rows_returned: 0,
rows_scanned: 0,
index_used: false,
tables: Vec::new(),
}
}
pub fn efficiency(&self) -> f64 {
if self.rows_scanned == 0 {
return 1.0;
}
self.rows_returned as f64 / self.rows_scanned as f64
}
pub fn is_slow(&self, threshold: Duration) -> bool {
self.execution_time > threshold
}
pub fn is_full_table_scan(&self) -> bool {
!self.index_used && self.rows_scanned > 1000
}
}
#[derive(Debug, Clone)]
pub struct IndexRecommendation {
pub table: String,
pub columns: Vec<String>,
pub reason: String,
pub priority: u8,
pub estimated_improvement: f64,
}
impl IndexRecommendation {
pub fn new(
table: impl Into<String>,
columns: Vec<String>,
reason: impl Into<String>,
priority: u8,
) -> Self {
Self {
table: table.into(),
columns,
reason: reason.into(),
priority,
estimated_improvement: 0.0,
}
}
pub fn with_estimated_improvement(mut self, improvement: f64) -> Self {
self.estimated_improvement = improvement;
self
}
pub fn generate_sql(&self) -> String {
let index_name = format!("idx_{}_{}", self.table, self.columns.join("_"));
format!(
"CREATE INDEX {} ON {} ({});",
index_name,
self.table,
self.columns.join(", ")
)
}
}
pub struct QueryAnalyzer {
stats: Vec<QueryStats>,
slow_query_threshold: Duration,
full_scan_threshold: u64,
}
impl QueryAnalyzer {
pub fn new() -> Self {
Self {
stats: Vec::new(),
slow_query_threshold: Duration::from_millis(100),
full_scan_threshold: 1000,
}
}
pub fn with_slow_query_threshold(mut self, threshold: Duration) -> Self {
self.slow_query_threshold = threshold;
self
}
pub fn with_full_scan_threshold(mut self, threshold: u64) -> Self {
self.full_scan_threshold = threshold;
self
}
pub fn add_stats(&mut self, stats: QueryStats) {
self.stats.push(stats);
}
pub fn slow_queries(&self) -> Vec<&QueryStats> {
self.stats
.iter()
.filter(|s| s.is_slow(self.slow_query_threshold))
.collect()
}
pub fn full_table_scans(&self) -> Vec<&QueryStats> {
self.stats
.iter()
.filter(|s| !s.index_used && s.rows_scanned > self.full_scan_threshold)
.collect()
}
pub fn recommend_indexes(&self) -> Vec<IndexRecommendation> {
let mut recommendations = Vec::new();
for stat in self.slow_queries() {
if !stat.index_used {
for table in &stat.tables {
let columns = self.extract_where_columns(&stat.query, table);
if !columns.is_empty() {
let rec = IndexRecommendation::new(
table.clone(),
columns,
format!("Slow query ({:?}), no index used", stat.execution_time),
5,
)
.with_estimated_improvement(50.0);
recommendations.push(rec);
}
}
}
}
for stat in self.full_table_scans() {
for table in &stat.tables {
let columns = self.extract_where_columns(&stat.query, table);
if !columns.is_empty() {
let rec = IndexRecommendation::new(
table.clone(),
columns,
format!("Full table scan ({} rows)", stat.rows_scanned),
4,
)
.with_estimated_improvement(70.0);
recommendations.push(rec);
}
}
}
self.deduplicate_recommendations(recommendations)
}
fn extract_where_columns(&self, query: &str, _table: &str) -> Vec<String> {
let mut columns = Vec::new();
let query_lower = query.to_lowercase();
if let Some(where_pos) = query_lower.find("where") {
let where_clause = &query_lower[where_pos..];
let parts: Vec<&str> = where_clause.split_whitespace().collect();
for i in 0..parts.len() {
if i + 1 < parts.len() {
let next = parts[i + 1];
if next.starts_with('=') || next == "in" || next == ">" || next == "<" {
let col = parts[i].trim_matches(|c: char| !c.is_alphanumeric() && c != '_');
if !col.is_empty() && col != "where" && col != "and" && col != "or" {
columns.push(col.to_string());
}
}
}
}
}
columns
}
fn deduplicate_recommendations(
&self,
recommendations: Vec<IndexRecommendation>,
) -> Vec<IndexRecommendation> {
let mut seen = HashSet::new();
let mut result = Vec::new();
for rec in recommendations {
let key = format!("{}:{}", rec.table, rec.columns.join(","));
if !seen.contains(&key) {
seen.insert(key);
result.push(rec);
}
}
result
}
pub fn summary(&self) -> QueryAnalysisSummary {
let total_queries = self.stats.len();
let slow_queries = self.slow_queries().len();
let full_scans = self.full_table_scans().len();
let avg_execution_time = if !self.stats.is_empty() {
self.stats
.iter()
.map(|s| s.execution_time.as_millis())
.sum::<u128>()
/ self.stats.len() as u128
} else {
0
};
QueryAnalysisSummary {
total_queries,
slow_queries,
full_table_scans: full_scans,
avg_execution_time: Duration::from_millis(avg_execution_time as u64),
}
}
}
impl Default for QueryAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct QueryAnalysisSummary {
pub total_queries: usize,
pub slow_queries: usize,
pub full_table_scans: usize,
pub avg_execution_time: Duration,
}
pub struct NPlusOneDetector {
query_patterns: HashMap<String, usize>,
threshold: usize,
}
impl NPlusOneDetector {
pub fn new() -> Self {
Self {
query_patterns: HashMap::new(),
threshold: 3,
}
}
pub fn with_threshold(mut self, threshold: usize) -> Self {
self.threshold = threshold;
self
}
pub fn record_query(&mut self, query: &str) {
let pattern = self.normalize_query(query);
*self.query_patterns.entry(pattern).or_insert(0) += 1;
}
pub fn detect(&self) -> Vec<NPlusOneProblem> {
let mut problems = Vec::new();
for (pattern, count) in &self.query_patterns {
if *count >= self.threshold {
problems.push(NPlusOneProblem {
query_pattern: pattern.clone(),
occurrence_count: *count,
suggestion: self.generate_suggestion(pattern),
});
}
}
problems.sort_by(|a, b| b.occurrence_count.cmp(&a.occurrence_count));
problems
}
fn normalize_query(&self, query: &str) -> String {
let mut normalized = query.to_lowercase();
normalized =
regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
.unwrap()
.replace_all(&normalized, "?")
.to_string();
normalized = regex::Regex::new(r"\b\d+\b")
.unwrap()
.replace_all(&normalized, "?")
.to_string();
normalized = regex::Regex::new(r"'[^']*'")
.unwrap()
.replace_all(&normalized, "?")
.to_string();
normalized
}
fn generate_suggestion(&self, pattern: &str) -> String {
if pattern.contains("select") && pattern.contains("where") {
"Consider using eager loading or batch fetching to reduce the number of queries"
} else {
"Consider optimizing this query pattern to reduce database round trips"
}
.to_string()
}
pub fn clear(&mut self) {
self.query_patterns.clear();
}
pub fn stats(&self) -> NPlusOneStats {
let total_patterns = self.query_patterns.len();
let total_queries: usize = self.query_patterns.values().sum();
let problematic_patterns = self
.query_patterns
.iter()
.filter(|(_, count)| **count >= self.threshold)
.count();
NPlusOneStats {
total_patterns,
total_queries,
problematic_patterns,
}
}
}
impl Default for NPlusOneDetector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct NPlusOneProblem {
pub query_pattern: String,
pub occurrence_count: usize,
pub suggestion: String,
}
#[derive(Debug, Clone)]
pub struct NPlusOneStats {
pub total_patterns: usize,
pub total_queries: usize,
pub problematic_patterns: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_stats_efficiency() {
let mut stats = QueryStats::new("SELECT * FROM users".to_string());
stats.rows_returned = 10;
stats.rows_scanned = 100;
assert_eq!(stats.efficiency(), 0.1);
}
#[test]
fn test_query_stats_is_slow() {
let mut stats = QueryStats::new("SELECT * FROM users".to_string());
stats.execution_time = Duration::from_millis(150);
assert!(stats.is_slow(Duration::from_millis(100)));
assert!(!stats.is_slow(Duration::from_millis(200)));
}
#[test]
fn test_index_recommendation_sql() {
let rec = IndexRecommendation::new(
"users",
vec!["email".to_string(), "created_at".to_string()],
"Slow query",
5,
);
let sql = rec.generate_sql();
assert!(sql.contains("CREATE INDEX"));
assert!(sql.contains("users"));
assert!(sql.contains("email"));
assert!(sql.contains("created_at"));
}
#[test]
fn test_query_analyzer_slow_queries() {
let mut analyzer =
QueryAnalyzer::new().with_slow_query_threshold(Duration::from_millis(100));
let mut slow_stats = QueryStats::new("SELECT * FROM users".to_string());
slow_stats.execution_time = Duration::from_millis(150);
analyzer.add_stats(slow_stats);
let mut fast_stats = QueryStats::new("SELECT * FROM tokens".to_string());
fast_stats.execution_time = Duration::from_millis(50);
analyzer.add_stats(fast_stats);
let slow_queries = analyzer.slow_queries();
assert_eq!(slow_queries.len(), 1);
assert!(slow_queries[0].query.contains("users"));
}
#[test]
fn test_n_plus_one_detector() {
let mut detector = NPlusOneDetector::new().with_threshold(3);
detector.record_query("SELECT * FROM orders WHERE user_id = '123'");
detector.record_query("SELECT * FROM orders WHERE user_id = '456'");
detector.record_query("SELECT * FROM orders WHERE user_id = '789'");
let problems = detector.detect();
assert_eq!(problems.len(), 1);
assert_eq!(problems[0].occurrence_count, 3);
}
#[test]
fn test_n_plus_one_detector_stats() {
let mut detector = NPlusOneDetector::new();
detector.record_query("SELECT * FROM users WHERE id = 1");
detector.record_query("SELECT * FROM users WHERE id = 2");
detector.record_query("SELECT * FROM tokens WHERE id = 1");
let stats = detector.stats();
assert_eq!(stats.total_queries, 3);
assert_eq!(stats.total_patterns, 2); }
}