use crate::timing::elapsed_nanos;
use crate::{prepare_postgres_sql, PreparedPostgresStatement, SqlStatement};
use hawdb_core::time::Instant;
use hawdb_core::Result;
use hawdb_plan_cache::{LfuCache, PlanCacheStats};
use std::sync::{Arc, Mutex, MutexGuard};
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct PreparedRelationalSql {
pub template: Arc<PreparedPostgresStatement>,
pub parse_nanos: u64,
}
impl PreparedRelationalSql {
pub fn statement(&self) -> &SqlStatement {
&self.template.statement
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct RelationalPlanTemplateCache {
entries: Mutex<LfuCache<String, Arc<PreparedPostgresStatement>>>,
}
impl RelationalPlanTemplateCache {
pub fn new(max_entries: Option<usize>) -> Self {
Self {
entries: Mutex::new(LfuCache::new(max_entries)),
}
}
pub fn prepare(&self, sql: &str) -> Result<PreparedRelationalSql> {
let key = sql.to_string();
if let Some(template) = self.entries().get(&key) {
return Ok(PreparedRelationalSql {
template,
parse_nanos: 0,
});
}
let started = Instant::now();
let template = Arc::new(prepare_postgres_sql(sql)?);
let parse_nanos = elapsed_nanos(started);
let mut entries = self.entries();
if is_cacheable_query(&template.statement) {
entries.insert(key, Arc::clone(&template));
} else {
entries.record_bypass();
}
Ok(PreparedRelationalSql {
template,
parse_nanos,
})
}
pub fn stats(&self) -> PlanCacheStats {
self.entries().stats()
}
fn entries(&self) -> MutexGuard<'_, LfuCache<String, Arc<PreparedPostgresStatement>>> {
self.entries
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
fn is_cacheable_query(statement: &SqlStatement) -> bool {
match statement {
SqlStatement::Select(_) => true,
SqlStatement::Explain(explain) => {
matches!(explain.statement.as_ref(), SqlStatement::Select(_))
}
SqlStatement::Insert(_)
| SqlStatement::Update(_)
| SqlStatement::Delete(_)
| SqlStatement::CreateTable(_)
| SqlStatement::CreateIndex(_)
| SqlStatement::AlterTableAddColumn(_)
| SqlStatement::Branch(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn branch_inspection_does_not_replace_data_query_templates() {
let cache = RelationalPlanTemplateCache::new(Some(1));
let first = cache.prepare("SELECT id FROM items WHERE id = $1").unwrap();
let before = cache.stats();
for sql in [
"SHOW BRANCHES LIMIT 2",
"SHOW BRANCHES LIMIT 2",
"SHOW CURRENT BRANCH",
] {
assert!(matches!(
cache.prepare(sql).unwrap().statement(),
SqlStatement::Branch(_)
));
}
let inspected = cache.stats();
assert_eq!(inspected.entries, before.entries);
assert_eq!(inspected.hits, before.hits);
let reused = cache.prepare("SELECT id FROM items WHERE id = $1").unwrap();
assert!(Arc::ptr_eq(&first.template, &reused.template));
assert_eq!(cache.stats().hits, before.hits + 1);
}
}