Phase 8: Hot cache + incremental delta updates

- MemCache: LRU in-memory cache for hot datasets (configurable max, default 16GB)
  Pin/evict/stats endpoints: POST /query/cache/pin, /cache/evict, GET /cache/stats
- Delta store: append-only delta Parquet files for row-level updates
  Write deltas without rewriting base files, merge at query time
- Compaction: POST /query/compact merges deltas into base Parquet
- Query engine: checks cache first, falls back to Parquet, merges deltas
- Benchmarked on 2.47M rows:
  1M row JOIN: 854ms cold → 96ms hot (8.9x speedup)
  100K filter: 62ms cold → 21ms hot (3x speedup)
  1.1M rows cached in 408MB RAM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-03-27 08:37:28 -05:00
co-authored by Claude Opus 4.6
parent eae51977ab
commit 6df904a03c
23 changed files with 518 additions and 131 deletions
+65
View File
@@ -9,12 +9,18 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use crate::cache::CacheStats;
use crate::context::QueryEngine;
use crate::delta;
pub fn router(engine: QueryEngine) -> Router {
Router::new()
.route("/health", get(health))
.route("/sql", post(execute_query))
.route("/cache/pin", post(pin_dataset))
.route("/cache/evict", post(evict_dataset))
.route("/cache/stats", get(cache_stats))
.route("/compact", post(compact_dataset))
.with_state(engine)
}
@@ -22,6 +28,8 @@ async fn health() -> &'static str {
"queryd ok"
}
// --- SQL Query ---
#[derive(Deserialize)]
struct QueryRequest {
sql: String,
@@ -87,3 +95,60 @@ async fn execute_query(
Err(e) => Err((StatusCode::BAD_REQUEST, e)),
}
}
// --- Cache Management ---
#[derive(Deserialize)]
struct CacheRequest {
dataset: String,
}
async fn pin_dataset(
State(engine): State<QueryEngine>,
Json(req): Json<CacheRequest>,
) -> impl IntoResponse {
match engine.pin_dataset(&req.dataset).await {
Ok(()) => Ok((StatusCode::OK, format!("pinned: {}", req.dataset))),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
async fn evict_dataset(
State(engine): State<QueryEngine>,
Json(req): Json<CacheRequest>,
) -> impl IntoResponse {
if engine.cache().evict(&req.dataset).await {
(StatusCode::OK, format!("evicted: {}", req.dataset))
} else {
(StatusCode::NOT_FOUND, format!("not cached: {}", req.dataset))
}
}
async fn cache_stats(State(engine): State<QueryEngine>) -> impl IntoResponse {
let stats = engine.cache().stats().await;
Json(stats)
}
// --- Compaction ---
#[derive(Deserialize)]
struct CompactRequest {
dataset: String,
base_key: String,
primary_key: Option<String>,
}
async fn compact_dataset(
State(engine): State<QueryEngine>,
Json(req): Json<CompactRequest>,
) -> impl IntoResponse {
match delta::compact(
engine.store(),
&req.dataset,
&req.base_key,
req.primary_key.as_deref(),
).await {
Ok(result) => Ok(Json(result)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}