use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone)]
pub struct HistoryConfig {
pub max_records: usize,
pub slow_query_threshold_ms: f64,
pub store_query_text: bool,
pub max_query_text_length: usize,
pub detailed_metrics: bool,
pub analysis_window_secs: u64,
pub top_queries_count: usize,
pub auto_cleanup: bool,
pub retention_period_secs: u64,
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
max_records: 10000,
slow_query_threshold_ms: 1000.0,
store_query_text: true,
max_query_text_length: 1000,
detailed_metrics: true,
analysis_window_secs: 3600, top_queries_count: 20,
auto_cleanup: true,
retention_period_secs: 86400, }
}
}
impl HistoryConfig {
pub fn minimal() -> Self {
Self {
max_records: 1000,
slow_query_threshold_ms: 500.0,
store_query_text: false,
max_query_text_length: 200,
detailed_metrics: false,
analysis_window_secs: 1800, top_queries_count: 10,
auto_cleanup: true,
retention_period_secs: 3600, }
}
pub fn comprehensive() -> Self {
Self {
max_records: 100000,
slow_query_threshold_ms: 2000.0,
store_query_text: true,
max_query_text_length: 5000,
detailed_metrics: true,
analysis_window_secs: 86400, top_queries_count: 50,
auto_cleanup: true,
retention_period_secs: 604800, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExecutionStatus {
Success,
Failed,
Cancelled,
Timeout,
}
impl fmt::Display for ExecutionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Success => write!(f, "SUCCESS"),
Self::Failed => write!(f, "FAILED"),
Self::Cancelled => write!(f, "CANCELLED"),
Self::Timeout => write!(f, "TIMEOUT"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QueryFormType {
Select,
Ask,
Construct,
Describe,
Update,
Unknown,
}
impl fmt::Display for QueryFormType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Select => write!(f, "SELECT"),
Self::Ask => write!(f, "ASK"),
Self::Construct => write!(f, "CONSTRUCT"),
Self::Describe => write!(f, "DESCRIBE"),
Self::Update => write!(f, "UPDATE"),
Self::Unknown => write!(f, "UNKNOWN"),
}
}
}
impl QueryFormType {
pub fn detect(query: &str) -> Self {
let query_upper = query.to_uppercase();
let query_trimmed = query_upper.trim();
if query_trimmed.starts_with("SELECT") {
Self::Select
} else if query_trimmed.starts_with("ASK") {
Self::Ask
} else if query_trimmed.starts_with("CONSTRUCT") {
Self::Construct
} else if query_trimmed.starts_with("DESCRIBE") {
Self::Describe
} else if query_trimmed.starts_with("INSERT")
|| query_trimmed.starts_with("DELETE")
|| query_trimmed.starts_with("LOAD")
|| query_trimmed.starts_with("CLEAR")
|| query_trimmed.starts_with("CREATE")
|| query_trimmed.starts_with("DROP")
{
Self::Update
} else {
Self::Unknown
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecutionMetrics {
pub planning_time_ms: f64,
pub execution_time_ms: f64,
pub serialization_time_ms: f64,
pub triples_scanned: usize,
pub joins_performed: usize,
pub peak_memory_bytes: usize,
pub cache_hits: usize,
pub cache_misses: usize,
pub index_lookups: usize,
}
impl ExecutionMetrics {
pub fn total_time_ms(&self) -> f64 {
self.planning_time_ms + self.execution_time_ms + self.serialization_time_ms
}
pub fn cache_hit_ratio(&self) -> f64 {
let total = self.cache_hits + self.cache_misses;
if total > 0 {
self.cache_hits as f64 / total as f64
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct ExecutionRecord {
pub id: u64,
pub query_text: Option<String>,
pub query_fingerprint: String,
pub query_form: QueryFormType,
pub status: ExecutionStatus,
pub execution_time_ms: f64,
pub result_count: usize,
pub started_at: SystemTime,
pub ended_at: SystemTime,
pub metrics: Option<ExecutionMetrics>,
pub user_id: Option<String>,
pub source: Option<String>,
pub error: Option<String>,
pub tags: Vec<String>,
}
impl ExecutionRecord {
pub fn new(query: impl Into<String>, execution_time_ms: f64, result_count: usize) -> Self {
let query_str = query.into();
let query_form = QueryFormType::detect(&query_str);
let fingerprint = Self::compute_fingerprint(&query_str);
let now = SystemTime::now();
Self {
id: Self::generate_id(),
query_text: Some(query_str),
query_fingerprint: fingerprint,
query_form,
status: ExecutionStatus::Success,
execution_time_ms,
result_count,
started_at: now,
ended_at: now,
metrics: None,
user_id: None,
source: None,
error: None,
tags: Vec::new(),
}
}
pub fn failed(query: impl Into<String>, error: impl Into<String>) -> Self {
let query_str = query.into();
let query_form = QueryFormType::detect(&query_str);
let fingerprint = Self::compute_fingerprint(&query_str);
let now = SystemTime::now();
Self {
id: Self::generate_id(),
query_text: Some(query_str),
query_fingerprint: fingerprint,
query_form,
status: ExecutionStatus::Failed,
execution_time_ms: 0.0,
result_count: 0,
started_at: now,
ended_at: now,
metrics: None,
user_id: None,
source: None,
error: Some(error.into()),
tags: Vec::new(),
}
}
pub fn with_metrics(mut self, metrics: ExecutionMetrics) -> Self {
self.metrics = Some(metrics);
self
}
pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn is_slow(&self, threshold_ms: f64) -> bool {
self.execution_time_ms >= threshold_ms
}
fn generate_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
fn compute_fingerprint(query: &str) -> String {
let normalized = query
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let fingerprint = normalized
.chars()
.filter(|c| {
c.is_alphabetic() || c.is_whitespace() || *c == '?' || *c == '{' || *c == '}'
})
.collect::<String>();
format!("{:x}", md5::compute(fingerprint.as_bytes()))
}
}
#[derive(Debug, Clone, Default)]
pub struct PeriodStatistics {
pub start_time: Option<SystemTime>,
pub end_time: Option<SystemTime>,
pub total_queries: usize,
pub successful_queries: usize,
pub failed_queries: usize,
pub total_execution_time_ms: f64,
pub avg_execution_time_ms: f64,
pub min_execution_time_ms: f64,
pub max_execution_time_ms: f64,
pub p95_execution_time_ms: f64,
pub total_results: usize,
pub queries_per_second: f64,
}
#[derive(Debug, Clone, Default)]
pub struct FormDistribution {
pub select_count: usize,
pub ask_count: usize,
pub construct_count: usize,
pub describe_count: usize,
pub update_count: usize,
pub unknown_count: usize,
}
impl FormDistribution {
fn add(&mut self, form: QueryFormType) {
match form {
QueryFormType::Select => self.select_count += 1,
QueryFormType::Ask => self.ask_count += 1,
QueryFormType::Construct => self.construct_count += 1,
QueryFormType::Describe => self.describe_count += 1,
QueryFormType::Update => self.update_count += 1,
QueryFormType::Unknown => self.unknown_count += 1,
}
}
fn total(&self) -> usize {
self.select_count
+ self.ask_count
+ self.construct_count
+ self.describe_count
+ self.update_count
+ self.unknown_count
}
}
#[derive(Debug, Clone)]
pub struct QueryGroupStats {
pub fingerprint: String,
pub sample_query: Option<String>,
pub query_form: QueryFormType,
pub execution_count: usize,
pub total_time_ms: f64,
pub avg_time_ms: f64,
pub min_time_ms: f64,
pub max_time_ms: f64,
pub success_rate: f64,
pub avg_result_count: f64,
pub last_executed: SystemTime,
}
#[derive(Debug, Clone)]
pub struct SlowQueryEntry {
pub record: ExecutionRecord,
pub rank: usize,
pub slowness_factor: f64,
}
#[derive(Debug, Clone)]
pub struct HistoryAnalysis {
pub generated_at: SystemTime,
pub overall_stats: PeriodStatistics,
pub form_distribution: FormDistribution,
pub top_by_frequency: Vec<QueryGroupStats>,
pub top_by_total_time: Vec<QueryGroupStats>,
pub slow_queries: Vec<SlowQueryEntry>,
pub hourly_stats: Vec<PeriodStatistics>,
pub error_rate: f64,
pub unique_queries: usize,
pub user_stats: HashMap<String, usize>,
}
impl HistoryAnalysis {
pub fn summary_text(&self) -> String {
let mut text = String::from("Query Execution History Analysis\n");
text.push_str(&format!("Generated: {:?}\n\n", self.generated_at));
text.push_str("Overall Statistics:\n");
text.push_str(&format!(
" Total Queries: {}\n",
self.overall_stats.total_queries
));
text.push_str(&format!(
" Success Rate: {:.2}%\n",
(1.0 - self.error_rate) * 100.0
));
text.push_str(&format!(
" Avg Execution Time: {:.2}ms\n",
self.overall_stats.avg_execution_time_ms
));
text.push_str(&format!(
" P95 Execution Time: {:.2}ms\n",
self.overall_stats.p95_execution_time_ms
));
text.push_str(&format!(" Unique Query Types: {}\n", self.unique_queries));
text.push_str("\nQuery Form Distribution:\n");
let total = self.form_distribution.total();
if total > 0 {
text.push_str(&format!(
" SELECT: {} ({:.1}%)\n",
self.form_distribution.select_count,
self.form_distribution.select_count as f64 / total as f64 * 100.0
));
text.push_str(&format!(
" ASK: {} ({:.1}%)\n",
self.form_distribution.ask_count,
self.form_distribution.ask_count as f64 / total as f64 * 100.0
));
text.push_str(&format!(
" CONSTRUCT: {} ({:.1}%)\n",
self.form_distribution.construct_count,
self.form_distribution.construct_count as f64 / total as f64 * 100.0
));
}
if !self.slow_queries.is_empty() {
text.push_str(&format!("\nSlow Queries: {}\n", self.slow_queries.len()));
}
text
}
}
#[derive(Debug)]
pub struct QueryExecutionHistory {
config: HistoryConfig,
records: VecDeque<ExecutionRecord>,
groups: HashMap<String, QueryGroupStats>,
stats: HistoryStatistics,
}
#[derive(Debug, Clone, Default)]
pub struct HistoryStatistics {
pub total_recorded: usize,
pub total_evicted: usize,
pub total_analyses: usize,
pub last_cleanup: Option<SystemTime>,
}
impl QueryExecutionHistory {
pub fn new(config: HistoryConfig) -> Self {
Self {
config,
records: VecDeque::new(),
groups: HashMap::new(),
stats: HistoryStatistics::default(),
}
}
pub fn with_defaults() -> Self {
Self::new(HistoryConfig::default())
}
pub fn record(&mut self, mut record: ExecutionRecord) {
if let Some(ref mut text) = record.query_text {
if !self.config.store_query_text {
record.query_text = None;
} else if text.len() > self.config.max_query_text_length {
text.truncate(self.config.max_query_text_length);
text.push_str("...");
}
}
self.update_group_stats(&record);
self.records.push_front(record);
self.stats.total_recorded += 1;
while self.records.len() > self.config.max_records {
self.records.pop_back();
self.stats.total_evicted += 1;
}
if self.config.auto_cleanup {
self.cleanup_old_records();
}
}
pub fn record_batch(&mut self, records: Vec<ExecutionRecord>) {
for record in records {
self.record(record);
}
}
pub fn recent(&self, count: usize) -> Vec<&ExecutionRecord> {
self.records.iter().take(count).collect()
}
pub fn slow_queries(&self, limit: usize) -> Vec<SlowQueryEntry> {
let avg_time = self.calculate_avg_time();
let threshold = self.config.slow_query_threshold_ms;
let mut slow: Vec<_> = self
.records
.iter()
.filter(|r| r.is_slow(threshold))
.collect();
slow.sort_by(|a, b| {
b.execution_time_ms
.partial_cmp(&a.execution_time_ms)
.unwrap_or(std::cmp::Ordering::Equal)
});
slow.iter()
.take(limit)
.enumerate()
.map(|(i, r)| SlowQueryEntry {
record: (*r).clone(),
rank: i + 1,
slowness_factor: if avg_time > 0.0 {
r.execution_time_ms / avg_time
} else {
1.0
},
})
.collect()
}
pub fn by_fingerprint(&self, fingerprint: &str) -> Vec<&ExecutionRecord> {
self.records
.iter()
.filter(|r| r.query_fingerprint == fingerprint)
.collect()
}
pub fn by_user(&self, user_id: &str) -> Vec<&ExecutionRecord> {
self.records
.iter()
.filter(|r| r.user_id.as_deref() == Some(user_id))
.collect()
}
pub fn by_status(&self, status: ExecutionStatus) -> Vec<&ExecutionRecord> {
self.records.iter().filter(|r| r.status == status).collect()
}
pub fn in_time_range(&self, start: SystemTime, end: SystemTime) -> Vec<&ExecutionRecord> {
self.records
.iter()
.filter(|r| r.started_at >= start && r.started_at <= end)
.collect()
}
pub fn analyze(&mut self) -> HistoryAnalysis {
self.stats.total_analyses += 1;
let overall_stats = self.calculate_period_stats(&self.records.iter().collect::<Vec<_>>());
let form_distribution = self.calculate_form_distribution();
let top_by_frequency = self.top_by_frequency();
let top_by_total_time = self.top_by_total_time();
let slow_queries = self.slow_queries(self.config.top_queries_count);
let hourly_stats = self.calculate_hourly_stats();
let user_stats = self.calculate_user_stats();
let error_count = self
.records
.iter()
.filter(|r| r.status != ExecutionStatus::Success)
.count();
let error_rate = if self.records.is_empty() {
0.0
} else {
error_count as f64 / self.records.len() as f64
};
HistoryAnalysis {
generated_at: SystemTime::now(),
overall_stats,
form_distribution,
top_by_frequency,
top_by_total_time,
slow_queries,
hourly_stats,
error_rate,
unique_queries: self.groups.len(),
user_stats,
}
}
pub fn group_stats(&self) -> &HashMap<String, QueryGroupStats> {
&self.groups
}
pub fn statistics(&self) -> &HistoryStatistics {
&self.stats
}
pub fn config(&self) -> &HistoryConfig {
&self.config
}
pub fn clear(&mut self) {
self.records.clear();
self.groups.clear();
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
fn update_group_stats(&mut self, record: &ExecutionRecord) {
let fingerprint = &record.query_fingerprint;
if let Some(group) = self.groups.get_mut(fingerprint) {
let n = group.execution_count as f64;
group.execution_count += 1;
group.total_time_ms += record.execution_time_ms;
group.avg_time_ms = group.total_time_ms / group.execution_count as f64;
group.min_time_ms = group.min_time_ms.min(record.execution_time_ms);
group.max_time_ms = group.max_time_ms.max(record.execution_time_ms);
let success_count = if record.status == ExecutionStatus::Success {
(group.success_rate * n) + 1.0
} else {
group.success_rate * n
};
group.success_rate = success_count / (n + 1.0);
group.avg_result_count =
(group.avg_result_count * n + record.result_count as f64) / (n + 1.0);
group.last_executed = record.started_at;
} else {
self.groups.insert(
fingerprint.clone(),
QueryGroupStats {
fingerprint: fingerprint.clone(),
sample_query: record.query_text.clone(),
query_form: record.query_form,
execution_count: 1,
total_time_ms: record.execution_time_ms,
avg_time_ms: record.execution_time_ms,
min_time_ms: record.execution_time_ms,
max_time_ms: record.execution_time_ms,
success_rate: if record.status == ExecutionStatus::Success {
1.0
} else {
0.0
},
avg_result_count: record.result_count as f64,
last_executed: record.started_at,
},
);
}
}
fn calculate_avg_time(&self) -> f64 {
if self.records.is_empty() {
return 0.0;
}
let total: f64 = self.records.iter().map(|r| r.execution_time_ms).sum();
total / self.records.len() as f64
}
fn calculate_period_stats(&self, records: &[&ExecutionRecord]) -> PeriodStatistics {
if records.is_empty() {
return PeriodStatistics::default();
}
let mut stats = PeriodStatistics {
start_time: records.last().map(|r| r.started_at),
end_time: records.first().map(|r| r.started_at),
total_queries: records.len(),
..Default::default()
};
let mut times: Vec<f64> = Vec::new();
for record in records {
if record.status == ExecutionStatus::Success {
stats.successful_queries += 1;
} else {
stats.failed_queries += 1;
}
stats.total_execution_time_ms += record.execution_time_ms;
stats.total_results += record.result_count;
times.push(record.execution_time_ms);
}
if !times.is_empty() {
times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
stats.avg_execution_time_ms = stats.total_execution_time_ms / times.len() as f64;
stats.min_execution_time_ms = times[0];
stats.max_execution_time_ms = times[times.len() - 1];
let p95_idx = ((times.len() as f64 * 0.95) as usize).min(times.len() - 1);
stats.p95_execution_time_ms = times[p95_idx];
}
if let (Some(start), Some(end)) = (stats.start_time, stats.end_time) {
if let Ok(duration) = end.duration_since(start) {
let secs = duration.as_secs_f64();
if secs > 0.0 {
stats.queries_per_second = stats.total_queries as f64 / secs;
}
}
}
stats
}
fn calculate_form_distribution(&self) -> FormDistribution {
let mut dist = FormDistribution::default();
for record in &self.records {
dist.add(record.query_form);
}
dist
}
fn top_by_frequency(&self) -> Vec<QueryGroupStats> {
let mut groups: Vec<_> = self.groups.values().cloned().collect();
groups.sort_by_key(|b| std::cmp::Reverse(b.execution_count));
groups.truncate(self.config.top_queries_count);
groups
}
fn top_by_total_time(&self) -> Vec<QueryGroupStats> {
let mut groups: Vec<_> = self.groups.values().cloned().collect();
groups.sort_by(|a, b| {
b.total_time_ms
.partial_cmp(&a.total_time_ms)
.unwrap_or(std::cmp::Ordering::Equal)
});
groups.truncate(self.config.top_queries_count);
groups
}
fn calculate_hourly_stats(&self) -> Vec<PeriodStatistics> {
let now = SystemTime::now();
let mut hourly_stats = Vec::new();
for hour in 0..24 {
let hour_start = now - Duration::from_secs((hour + 1) * 3600);
let hour_end = now - Duration::from_secs(hour * 3600);
let records: Vec<_> = self
.records
.iter()
.filter(|r| r.started_at >= hour_start && r.started_at < hour_end)
.collect();
hourly_stats.push(self.calculate_period_stats(&records));
}
hourly_stats
}
fn calculate_user_stats(&self) -> HashMap<String, usize> {
let mut user_stats = HashMap::new();
for record in &self.records {
if let Some(ref user_id) = record.user_id {
*user_stats.entry(user_id.clone()).or_insert(0) += 1;
}
}
user_stats
}
fn cleanup_old_records(&mut self) {
let now = SystemTime::now();
let cutoff = now - Duration::from_secs(self.config.retention_period_secs);
let original_len = self.records.len();
self.records.retain(|r| r.started_at >= cutoff);
self.stats.total_evicted += original_len - self.records.len();
self.stats.last_cleanup = Some(now);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_record_creation() {
let record = ExecutionRecord::new("SELECT ?s WHERE { ?s ?p ?o }", 10.5, 100);
assert!(record.query_text.is_some());
assert_eq!(record.execution_time_ms, 10.5);
assert_eq!(record.result_count, 100);
assert_eq!(record.status, ExecutionStatus::Success);
assert_eq!(record.query_form, QueryFormType::Select);
}
#[test]
fn test_failed_record() {
let record = ExecutionRecord::failed("SELECT * WHERE { ?s ?p ?o }", "Syntax error");
assert_eq!(record.status, ExecutionStatus::Failed);
assert_eq!(record.error, Some("Syntax error".to_string()));
}
#[test]
fn test_query_form_detection() {
assert_eq!(
QueryFormType::detect("SELECT ?s WHERE { ?s ?p ?o }"),
QueryFormType::Select
);
assert_eq!(
QueryFormType::detect("ASK WHERE { ?s ?p ?o }"),
QueryFormType::Ask
);
assert_eq!(
QueryFormType::detect("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"),
QueryFormType::Construct
);
assert_eq!(
QueryFormType::detect("DESCRIBE <http://example.org>"),
QueryFormType::Describe
);
assert_eq!(
QueryFormType::detect("INSERT DATA { <s> <p> <o> }"),
QueryFormType::Update
);
}
#[test]
fn test_history_record() {
let mut history = QueryExecutionHistory::with_defaults();
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
10.0,
50,
));
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
15.0,
60,
));
assert_eq!(history.len(), 2);
assert_eq!(history.stats.total_recorded, 2);
}
#[test]
fn test_slow_query_detection() {
let config = HistoryConfig {
slow_query_threshold_ms: 100.0,
..Default::default()
};
let mut history = QueryExecutionHistory::new(config);
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
50.0,
10,
));
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
150.0,
20,
));
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
200.0,
30,
));
let slow = history.slow_queries(10);
assert_eq!(slow.len(), 2);
assert_eq!(slow[0].rank, 1);
assert!(slow[0].record.execution_time_ms >= slow[1].record.execution_time_ms);
}
#[test]
fn test_group_statistics() {
let mut history = QueryExecutionHistory::with_defaults();
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
10.0,
50,
));
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
20.0,
60,
));
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
30.0,
70,
));
let groups = history.group_stats();
assert_eq!(groups.len(), 1);
let group = groups.values().next().unwrap();
assert_eq!(group.execution_count, 3);
assert_eq!(group.avg_time_ms, 20.0);
assert_eq!(group.min_time_ms, 10.0);
assert_eq!(group.max_time_ms, 30.0);
}
#[test]
fn test_analysis() {
let mut history = QueryExecutionHistory::with_defaults();
for i in 0..10 {
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
(i as f64) * 10.0,
i * 10,
));
}
let analysis = history.analyze();
assert_eq!(analysis.overall_stats.total_queries, 10);
assert!(analysis.overall_stats.avg_execution_time_ms > 0.0);
}
#[test]
fn test_by_status() {
let mut history = QueryExecutionHistory::with_defaults();
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
10.0,
50,
));
history.record(ExecutionRecord::failed(
"SELECT ?s WHERE { ?s ?p ?o }",
"Error",
));
let successes = history.by_status(ExecutionStatus::Success);
let failures = history.by_status(ExecutionStatus::Failed);
assert_eq!(successes.len(), 1);
assert_eq!(failures.len(), 1);
}
#[test]
fn test_by_user() {
let mut history = QueryExecutionHistory::with_defaults();
history.record(
ExecutionRecord::new("SELECT ?s WHERE { ?s ?p ?o }", 10.0, 50).with_user("alice"),
);
history.record(
ExecutionRecord::new("SELECT ?s WHERE { ?s ?p ?o }", 15.0, 60).with_user("bob"),
);
history.record(
ExecutionRecord::new("SELECT ?s WHERE { ?s ?p ?o }", 20.0, 70).with_user("alice"),
);
let alice_queries = history.by_user("alice");
assert_eq!(alice_queries.len(), 2);
}
#[test]
fn test_max_records_enforcement() {
let config = HistoryConfig {
max_records: 5,
..Default::default()
};
let mut history = QueryExecutionHistory::new(config);
for i in 0..10 {
history.record(ExecutionRecord::new(
format!("SELECT ?s{} WHERE {{ ?s ?p ?o }}", i),
10.0,
50,
));
}
assert_eq!(history.len(), 5);
assert_eq!(history.stats.total_evicted, 5);
}
#[test]
fn test_form_distribution() {
let mut history = QueryExecutionHistory::with_defaults();
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
10.0,
50,
));
history.record(ExecutionRecord::new("ASK WHERE { ?s ?p ?o }", 5.0, 1));
history.record(ExecutionRecord::new(
"CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }",
15.0,
30,
));
let analysis = history.analyze();
assert_eq!(analysis.form_distribution.select_count, 1);
assert_eq!(analysis.form_distribution.ask_count, 1);
assert_eq!(analysis.form_distribution.construct_count, 1);
}
#[test]
fn test_execution_metrics() {
let metrics = ExecutionMetrics {
planning_time_ms: 5.0,
execution_time_ms: 50.0,
serialization_time_ms: 10.0,
cache_hits: 80,
cache_misses: 20,
..Default::default()
};
assert_eq!(metrics.total_time_ms(), 65.0);
assert_eq!(metrics.cache_hit_ratio(), 0.8);
}
#[test]
fn test_config_presets() {
let minimal = HistoryConfig::minimal();
assert_eq!(minimal.max_records, 1000);
assert!(!minimal.store_query_text);
let comprehensive = HistoryConfig::comprehensive();
assert_eq!(comprehensive.max_records, 100000);
assert!(comprehensive.store_query_text);
}
#[test]
fn test_clear_history() {
let mut history = QueryExecutionHistory::with_defaults();
history.record(ExecutionRecord::new(
"SELECT ?s WHERE { ?s ?p ?o }",
10.0,
50,
));
assert!(!history.is_empty());
history.clear();
assert!(history.is_empty());
}
#[test]
fn test_execution_status_display() {
assert_eq!(format!("{}", ExecutionStatus::Success), "SUCCESS");
assert_eq!(format!("{}", ExecutionStatus::Failed), "FAILED");
assert_eq!(format!("{}", ExecutionStatus::Cancelled), "CANCELLED");
assert_eq!(format!("{}", ExecutionStatus::Timeout), "TIMEOUT");
}
#[test]
fn test_record_with_tags() {
let record = ExecutionRecord::new("SELECT ?s WHERE { ?s ?p ?o }", 10.0, 50)
.with_tag("batch")
.with_tag("priority");
assert_eq!(record.tags.len(), 2);
assert!(record.tags.contains(&"batch".to_string()));
}
}