use crate::api::select_cache::QueryPattern;
use crate::api::SelectCacheStats;
use crate::AppState;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Json, Response},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct CacheStatsResponse {
pub stats: SelectCacheStats,
pub timestamp: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CacheClearResponse {
pub status: String,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CacheInvalidateResponse {
pub status: String,
pub etag: String,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: String,
pub details: Option<String>,
}
impl IntoResponse for ErrorResponse {
fn into_response(self) -> Response {
let body = Json(self);
(StatusCode::BAD_REQUEST, body).into_response()
}
}
pub async fn get_cache_stats(
State(state): State<AppState>,
) -> Result<Json<CacheStatsResponse>, ErrorResponse> {
let stats = state.select_result_cache.stats().await;
Ok(Json(CacheStatsResponse {
stats,
timestamp: chrono::Utc::now().to_rfc3339(),
}))
}
pub async fn clear_cache(
State(state): State<AppState>,
) -> Result<Json<CacheClearResponse>, ErrorResponse> {
state.select_result_cache.clear().await;
Ok(Json(CacheClearResponse {
status: "success".to_string(),
message: "Cache cleared successfully".to_string(),
}))
}
pub async fn invalidate_object_cache(
State(state): State<AppState>,
Path(etag): Path<String>,
) -> Result<Json<CacheInvalidateResponse>, ErrorResponse> {
state.select_result_cache.invalidate_object(&etag).await;
Ok(Json(CacheInvalidateResponse {
status: "success".to_string(),
etag,
message: "Cache invalidated for object".to_string(),
}))
}
#[derive(Debug, Deserialize)]
pub struct TopQueriesParams {
#[serde(default = "default_limit")]
pub limit: usize,
}
fn default_limit() -> usize {
10
}
#[derive(Debug, Deserialize)]
pub struct RecentQueriesParams {
#[serde(default = "default_time_window")]
pub seconds: i64,
}
fn default_time_window() -> i64 {
3600
}
#[derive(Debug, Serialize)]
pub struct PatternStatsResponse {
pub stats: std::collections::HashMap<String, serde_json::Value>,
pub timestamp: String,
}
#[derive(Debug, Serialize)]
pub struct TopQueriesResponse {
pub queries: Vec<QueryPattern>,
pub timestamp: String,
}
#[derive(Debug, Serialize)]
pub struct RecentQueriesResponse {
pub queries: Vec<QueryPattern>,
pub window_seconds: i64,
pub timestamp: String,
}
#[derive(Debug, Serialize)]
pub struct PatternClearResponse {
pub status: String,
pub message: String,
}
pub async fn get_pattern_stats(
State(state): State<AppState>,
) -> Result<Json<PatternStatsResponse>, ErrorResponse> {
let stats = state.select_result_cache.pattern_stats().await;
Ok(Json(PatternStatsResponse {
stats,
timestamp: chrono::Utc::now().to_rfc3339(),
}))
}
pub async fn get_top_queries(
State(state): State<AppState>,
Query(params): Query<TopQueriesParams>,
) -> Result<Json<TopQueriesResponse>, ErrorResponse> {
let queries = state
.select_result_cache
.get_top_queries(params.limit)
.await;
Ok(Json(TopQueriesResponse {
queries,
timestamp: chrono::Utc::now().to_rfc3339(),
}))
}
pub async fn get_recent_queries(
State(state): State<AppState>,
Query(params): Query<RecentQueriesParams>,
) -> Result<Json<RecentQueriesResponse>, ErrorResponse> {
let queries = state
.select_result_cache
.get_recent_queries(params.seconds)
.await;
Ok(Json(RecentQueriesResponse {
queries,
window_seconds: params.seconds,
timestamp: chrono::Utc::now().to_rfc3339(),
}))
}
pub async fn clear_patterns(
State(state): State<AppState>,
) -> Result<Json<PatternClearResponse>, ErrorResponse> {
state.select_result_cache.clear_patterns().await;
Ok(Json(PatternClearResponse {
status: "success".to_string(),
message: "Query patterns cleared successfully".to_string(),
}))
}
#[derive(Debug, Serialize)]
pub struct CacheSaveResponse {
pub status: String,
pub message: String,
pub entries_saved: usize,
pub patterns_saved: usize,
}
#[derive(Debug, Serialize)]
pub struct CacheLoadResponse {
pub status: String,
pub message: String,
pub entries_loaded: usize,
pub entries_expired: usize,
pub patterns_loaded: usize,
}
pub async fn save_cache(
State(state): State<AppState>,
) -> Result<Json<CacheSaveResponse>, ErrorResponse> {
let cache_path = std::path::PathBuf::from(&state.config.storage_root)
.join(".cache")
.join("select_cache.json");
if let Some(parent) = cache_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| ErrorResponse {
error: "Failed to create cache directory".to_string(),
details: Some(e.to_string()),
})?;
}
let cache_stats = state.select_result_cache.stats().await;
let pattern_stats: std::collections::HashMap<String, serde_json::Value> =
state.select_result_cache.pattern_stats().await;
state
.select_result_cache
.save_to_file(&cache_path)
.await
.map_err(
|e: Box<dyn std::error::Error + Send + Sync>| ErrorResponse {
error: "Failed to save cache".to_string(),
details: Some(e.to_string()),
},
)?;
Ok(Json(CacheSaveResponse {
status: "success".to_string(),
message: format!("Cache saved to {}", cache_path.display()),
entries_saved: cache_stats.current_entries,
patterns_saved: pattern_stats
.get("total_patterns")
.and_then(|v: &serde_json::Value| v.as_u64())
.unwrap_or(0) as usize,
}))
}
pub async fn load_cache(
State(state): State<AppState>,
) -> Result<Json<CacheLoadResponse>, ErrorResponse> {
let cache_path = std::path::PathBuf::from(&state.config.storage_root)
.join(".cache")
.join("select_cache.json");
if !cache_path.exists() {
return Err(ErrorResponse {
error: "Cache file not found".to_string(),
details: Some(format!("No cache file at {}", cache_path.display())),
});
}
let stats_before = state.select_result_cache.stats().await;
state
.select_result_cache
.load_from_file(&cache_path)
.await
.map_err(
|e: Box<dyn std::error::Error + Send + Sync>| ErrorResponse {
error: "Failed to load cache".to_string(),
details: Some(e.to_string()),
},
)?;
let stats_after = state.select_result_cache.stats().await;
let pattern_stats: std::collections::HashMap<String, serde_json::Value> =
state.select_result_cache.pattern_stats().await;
let entries_expired = (stats_after.expirations - stats_before.expirations) as usize;
Ok(Json(CacheLoadResponse {
status: "success".to_string(),
message: format!("Cache loaded from {}", cache_path.display()),
entries_loaded: stats_after.current_entries,
entries_expired,
patterns_loaded: pattern_stats
.get("total_patterns")
.and_then(|v: &serde_json::Value| v.as_u64())
.unwrap_or(0) as usize,
}))
}
#[cfg(test)]
mod tests {
}