use std::num::NonZeroUsize;
use std::sync::Arc;
use lru::LruCache;
use parking_lot::Mutex;
use sqlparser::ast::Statement;
use crate::sql::Parser;
use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct CompiledPlan {
pub name: String,
pub sql: String,
pub statement: Arc<Statement>,
pub execution_count: u64,
}
pub struct CompiledPlanCache {
inner: Mutex<LruCache<String, CompiledPlan>>,
parser: Parser,
}
impl CompiledPlanCache {
pub const DEFAULT_CAPACITY: usize = 256;
#[must_use]
pub fn new() -> Self {
Self::with_capacity(Self::DEFAULT_CAPACITY)
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
let cap = NonZeroUsize::new(capacity.max(1)).unwrap_or(
#[allow(clippy::unwrap_used)]
NonZeroUsize::new(1).unwrap(),
);
Self {
inner: Mutex::new(LruCache::new(cap)),
parser: Parser::new(),
}
}
pub fn compile(&self, name: impl Into<String>, sql: impl Into<String>) -> Result<CompiledPlan> {
let name = name.into();
let sql = sql.into();
let statement = self.parser.parse_one(&sql)?;
let plan = CompiledPlan {
name: name.clone(),
sql,
statement: Arc::new(statement),
execution_count: 0,
};
self.inner.lock().put(name, plan.clone());
Ok(plan)
}
pub fn execute(&self, name: &str) -> Option<CompiledPlan> {
let mut guard = self.inner.lock();
let plan = guard.get_mut(name)?;
plan.execution_count = plan.execution_count.saturating_add(1);
Some(plan.clone())
}
pub fn peek(&self, name: &str) -> Option<CompiledPlan> {
self.inner.lock().peek(name).cloned()
}
pub fn drop_plan(&self, name: &str) -> bool {
self.inner.lock().pop(name).is_some()
}
pub fn len(&self) -> usize {
self.inner.lock().len()
}
pub fn is_empty(&self) -> bool {
self.inner.lock().is_empty()
}
pub fn clear(&self) {
self.inner.lock().clear();
}
pub fn snapshot(&self) -> Vec<(String, u64, String)> {
let guard = self.inner.lock();
let mut out: Vec<_> = guard
.iter()
.map(|(name, plan)| (name.clone(), plan.execution_count, plan.sql.clone()))
.collect();
out.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
out
}
}
impl Default for CompiledPlanCache {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn parse_prepare_compiled(sql: &str) -> Option<(String, String)> {
let trimmed = sql.trim().trim_end_matches(';').trim();
let upper: String = trimmed.chars().take(16).collect::<String>().to_ascii_uppercase();
if !upper.starts_with("PREPARE COMPILED") {
return None;
}
let rest = trimmed[16..].trim_start();
let as_idx = rest.to_ascii_uppercase().find(" AS ")?;
let name = rest[..as_idx].trim().to_string();
let inner = rest[as_idx + 4..].trim().to_string();
if name.is_empty() || inner.is_empty() {
return None;
}
Some((name, inner))
}
#[must_use]
pub fn parse_execute(sql: &str) -> Option<String> {
let trimmed = sql.trim().trim_end_matches(';').trim();
let mut iter = trimmed.split_ascii_whitespace();
let kw = iter.next()?;
if !kw.eq_ignore_ascii_case("EXECUTE") {
return None;
}
let name = iter.next()?;
let name = name.split('(').next().unwrap_or(name).trim().to_string();
if name.is_empty() {
return None;
}
Some(name)
}
pub fn try_handle_compiled(cache: &CompiledPlanCache, sql: &str) -> Result<Option<CompiledPlan>> {
if let Some((name, inner)) = parse_prepare_compiled(sql) {
let plan = cache.compile(name, inner)?;
return Ok(Some(plan));
}
if let Some(name) = parse_execute(sql) {
let plan = cache
.execute(&name)
.ok_or_else(|| Error::sql_parse(format!("compiled plan '{name}' not found")))?;
return Ok(Some(plan));
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_prepare_compiled_matches_canonical_form() {
let (name, sql) = parse_prepare_compiled("PREPARE COMPILED top_users AS SELECT * FROM users").expect("matches");
assert_eq!(name, "top_users");
assert_eq!(sql, "SELECT * FROM users");
}
#[test]
fn parse_prepare_compiled_case_insensitive() {
let (name, sql) = parse_prepare_compiled("prepare compiled q1 As select 1").expect("matches");
assert_eq!(name, "q1");
assert_eq!(sql, "select 1");
}
#[test]
fn parse_prepare_compiled_rejects_plain_sql() {
assert!(parse_prepare_compiled("SELECT 1").is_none());
assert!(parse_prepare_compiled("PREPARE foo AS SELECT 1").is_none());
assert!(parse_prepare_compiled("PREPARE COMPILED AS SELECT 1").is_none());
assert!(parse_prepare_compiled("PREPARE COMPILED foo AS").is_none());
}
#[test]
fn parse_execute_strips_parens() {
assert_eq!(parse_execute("EXECUTE q1").as_deref(), Some("q1"));
assert_eq!(parse_execute("execute q1(1, 2)").as_deref(), Some("q1"));
assert_eq!(parse_execute("EXECUTE q1 ;").as_deref(), Some("q1"));
assert!(parse_execute("EXECUTE").is_none());
assert!(parse_execute("SELECT 1").is_none());
}
#[test]
fn cache_compile_and_execute_roundtrip() {
let cache = CompiledPlanCache::with_capacity(8);
let plan = cache.compile("hot1", "SELECT 1").expect("compile");
assert_eq!(plan.execution_count, 0);
assert_eq!(cache.len(), 1);
let p1 = cache.execute("hot1").expect("execute");
let p2 = cache.execute("hot1").expect("execute");
assert_eq!(p1.execution_count, 1);
assert_eq!(p2.execution_count, 2);
}
#[test]
fn cache_unknown_plan_returns_none() {
let cache = CompiledPlanCache::new();
assert!(cache.execute("nope").is_none());
}
#[test]
fn cache_lru_eviction_kicks_in() {
let cache = CompiledPlanCache::with_capacity(2);
let _ = cache.compile("a", "SELECT 1").unwrap();
let _ = cache.compile("b", "SELECT 2").unwrap();
let _ = cache.compile("c", "SELECT 3").unwrap();
assert_eq!(cache.len(), 2);
assert!(cache.peek("a").is_none());
assert!(cache.peek("b").is_some());
assert!(cache.peek("c").is_some());
}
#[test]
fn cache_drop_and_clear() {
let cache = CompiledPlanCache::new();
cache.compile("x", "SELECT 1").unwrap();
cache.compile("y", "SELECT 2").unwrap();
assert!(cache.drop_plan("x"));
assert!(!cache.drop_plan("x"));
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn snapshot_orders_by_execution_count() {
let cache = CompiledPlanCache::new();
cache.compile("a", "SELECT 1").unwrap();
cache.compile("b", "SELECT 2").unwrap();
for _ in 0..5 {
cache.execute("b").unwrap();
}
cache.execute("a").unwrap();
let snap = cache.snapshot();
assert_eq!(snap[0].0, "b");
assert_eq!(snap[0].1, 5);
assert_eq!(snap[1].0, "a");
assert_eq!(snap[1].1, 1);
}
#[test]
fn try_handle_compiled_compiles_then_executes() {
let cache = CompiledPlanCache::new();
let p = try_handle_compiled(&cache, "PREPARE COMPILED q AS SELECT 1")
.expect("ok")
.expect("matched");
assert_eq!(p.name, "q");
let p2 = try_handle_compiled(&cache, "EXECUTE q").expect("ok").expect("matched");
assert_eq!(p2.execution_count, 1);
assert!(try_handle_compiled(&cache, "SELECT 42").unwrap().is_none());
}
#[test]
fn try_handle_compiled_errors_on_unknown_execute() {
let cache = CompiledPlanCache::new();
let r = try_handle_compiled(&cache, "EXECUTE missing");
assert!(r.is_err());
}
}