#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PredicateType {
Equality,
Range,
In,
Like,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QueryPattern {
pub labels: Vec<String>,
pub properties: Vec<String>,
pub predicates: Vec<PredicateType>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PatternStats {
pub count: u64,
pub total_time_ms: u64,
pub avg_time_ms: f64,
pub last_seen_ms: u64,
}
#[allow(dead_code)]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct QueryPatternTracker {
patterns: HashMap<QueryPattern, PatternStats>,
slow_query_threshold_ms: u64,
}
#[allow(dead_code)]
impl QueryPatternTracker {
#[must_use]
pub fn new() -> Self {
Self {
patterns: HashMap::new(),
slow_query_threshold_ms: 100,
}
}
pub fn set_threshold(&mut self, threshold_ms: u64) {
self.slow_query_threshold_ms = threshold_ms;
}
pub fn record(&mut self, pattern: QueryPattern, execution_time_ms: u64) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let stats = self.patterns.entry(pattern).or_default();
stats.count += 1;
stats.total_time_ms += execution_time_ms;
#[allow(clippy::cast_precision_loss)]
{
stats.avg_time_ms = stats.total_time_ms as f64 / stats.count as f64;
}
stats.last_seen_ms = now_ms;
}
#[must_use]
pub fn expensive_patterns(&self) -> Vec<(&QueryPattern, &PatternStats)> {
let mut patterns: Vec<_> = self.patterns.iter().collect();
patterns.sort_by(|a, b| b.1.total_time_ms.cmp(&a.1.total_time_ms));
patterns
}
#[must_use]
pub fn slow_patterns(&self) -> Vec<(&QueryPattern, &PatternStats)> {
self.patterns
.iter()
.filter(|(_, stats)| stats.avg_time_ms > self.slow_query_threshold_ms as f64)
.collect()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSuggestion {
pub ddl: String,
pub pattern: QueryPattern,
pub estimated_improvement: f64,
pub query_count: u64,
pub priority_score: f64,
}
#[allow(dead_code)]
#[derive(Debug, Default)]
pub struct IndexAdvisor {
existing_indexes: std::collections::HashSet<String>,
}
#[allow(dead_code)]
impl IndexAdvisor {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register_index(&mut self, name: impl Into<String>) {
self.existing_indexes.insert(name.into());
}
#[must_use]
pub fn suggest(&self, tracker: &QueryPatternTracker) -> Vec<IndexSuggestion> {
let mut suggestions = Vec::new();
for (pattern, stats) in tracker.expensive_patterns() {
if pattern.properties.is_empty() || pattern.labels.is_empty() {
continue;
}
let index_name = format!(
"idx_{}_{}",
pattern.labels.join("_").to_lowercase(),
pattern.properties.join("_").to_lowercase()
);
if self.existing_indexes.contains(&index_name) {
continue;
}
let improvement = Self::estimate_improvement(pattern);
if improvement < 0.2 {
continue;
}
let priority = stats.count as f64 * improvement * stats.avg_time_ms;
let ddl = format!(
"CREATE INDEX {} ON :{}({})",
index_name,
pattern.labels.first().unwrap_or(&String::new()),
pattern.properties.join(", ")
);
suggestions.push(IndexSuggestion {
ddl,
pattern: pattern.clone(),
estimated_improvement: improvement,
query_count: stats.count,
priority_score: priority,
});
}
suggestions.sort_by(|a, b| {
b.priority_score
.partial_cmp(&a.priority_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
suggestions
}
fn estimate_improvement(pattern: &QueryPattern) -> f64 {
let mut improvement = 0.0;
for pred in &pattern.predicates {
match pred {
PredicateType::Equality => improvement += 0.9,
PredicateType::Range => improvement += 0.7,
PredicateType::In => improvement += 0.6,
PredicateType::Like => improvement += 0.3,
}
}
(improvement / pattern.predicates.len().max(1) as f64).min(1.0)
}
}