use super::super::system::{is_protected_collection, AppState};
use crate::{
error::DbError,
storage::{http_client::get_http_client, query_cache},
sync::{LogEntry, Operation},
};
use axum::{
extract::{Path, State},
http::HeaderMap,
response::Json,
};
use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Deserialize)]
pub struct PruneRequest {
pub older_than: String,
}
pub async fn truncate_collection(
State(state): State<AppState>,
Path((db_name, coll_name)): Path<(String, String)>,
headers: HeaderMap,
) -> Result<Json<Value>, DbError> {
if is_protected_collection(&db_name, &coll_name) {
return Err(DbError::BadRequest(format!(
"Cannot truncate protected system collection: {}",
coll_name
)));
}
let database = state.storage.get_database(&db_name)?;
let collection = database.get_collection(&coll_name)?;
let is_shard_direct = headers.contains_key("X-Shard-Direct");
let saved_shard_config = collection.get_shard_config();
let mut total_count = 0usize;
if let Some(ref shard_config) = saved_shard_config {
if shard_config.num_shards > 0 {
let remote_nodes: Vec<(String, String)> = if !is_shard_direct {
if let Some(ref mgr) = state.cluster_manager {
let my_id = mgr.local_node_id();
mgr.state()
.get_all_members()
.into_iter()
.filter(|m| m.node.id != my_id)
.map(|m| (m.node.id.clone(), m.node.api_address.clone()))
.collect()
} else {
vec![]
}
} else {
vec![]
};
for shard_id in 0..shard_config.num_shards {
let physical_name = format!("{}_s{}", coll_name, shard_id);
if let Ok(shard_coll) = database.get_collection(&physical_name) {
let c = shard_coll.clone();
if let Ok(count) = tokio::task::spawn_blocking(move || c.truncate())
.await
.map_err(|e| DbError::InternalError(format!("Task error: {}", e)))?
{
total_count += count;
}
}
}
if !remote_nodes.is_empty() {
let client = get_http_client();
let secret = state.cluster_secret();
let auth_header = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
for shard_id in 0..shard_config.num_shards {
let physical_name = format!("{}_s{}", coll_name, shard_id);
for (_node_id, addr) in &remote_nodes {
let url = format!(
"http://{}/_api/database/{}/collection/{}/truncate",
addr, db_name, physical_name
);
let mut req = client
.put(&url)
.header("X-Shard-Direct", "true")
.header("X-Cluster-Secret", &secret)
.timeout(std::time::Duration::from_secs(10));
if !auth_header.is_empty() {
req = req.header("Authorization", &auth_header);
}
let _ = req.send().await;
}
}
}
}
}
let coll = collection.clone();
let count = tokio::task::spawn_blocking(move || coll.truncate())
.await
.map_err(|e| DbError::InternalError(format!("Task error: {}", e)))??;
total_count += count;
if let Some(config) = saved_shard_config.clone() {
let _ = collection.set_shard_config(&config);
}
query_cache::get_query_cache().invalidate_collection(&coll_name);
if !is_shard_direct {
if let Some(ref log) = state.replication_log {
let entry = LogEntry {
sequence: 0,
node_id: "".to_string(),
database: db_name.clone(),
collection: coll_name.clone(),
operation: Operation::TruncateCollection,
key: "".to_string(),
data: None,
timestamp: chrono::Utc::now().timestamp_millis() as u64,
origin_sequence: None,
};
let _ = log.append(entry);
}
}
Ok(Json(serde_json::json!({
"database": db_name,
"collection": coll_name,
"deleted": total_count,
"status": "truncated"
})))
}
pub async fn compact_collection(
State(state): State<AppState>,
Path((db_name, coll_name)): Path<(String, String)>,
) -> Result<Json<Value>, DbError> {
let database = state.storage.get_database(&db_name)?;
let collection = database.get_collection(&coll_name)?;
collection.compact();
Ok(Json(serde_json::json!({
"database": db_name,
"collection": coll_name,
"status": "compacted"
})))
}
pub async fn repair_collection(
State(state): State<AppState>,
Path((db_name, coll_name)): Path<(String, String)>,
) -> Result<Json<Value>, DbError> {
if let Some(coordinator) = state.shard_coordinator {
let report = coordinator
.repair_collection(&db_name, &coll_name)
.await
.map_err(DbError::InternalError)?;
Ok(Json(serde_json::json!({
"status": "repaired",
"report": report
})))
} else {
Err(DbError::InternalError(
"Shard coordinator not available".to_string(),
))
}
}
pub async fn prune_collection(
State(state): State<AppState>,
Path((db_name, coll_name)): Path<(String, String)>,
Json(payload): Json<PruneRequest>,
) -> Result<Json<Value>, DbError> {
let database = state.storage.get_database(&db_name)?;
let collection = database.get_collection(&coll_name)?;
let dt = chrono::DateTime::parse_from_rfc3339(&payload.older_than).map_err(|_| {
DbError::BadRequest("Invalid timestamp format (ISO8601 required)".to_string())
})?;
let timestamp_ms = dt.timestamp_millis();
if timestamp_ms < 0 {
return Err(DbError::BadRequest(
"Timestamp cannot be negative".to_string(),
));
}
let count = collection.prune_older_than(timestamp_ms as u64)?;
Ok(Json(serde_json::json!({ "deleted": count })))
}