use crate::keys;
use crate::region::{Region, Strategy};
use crate::stats::{Event, Stats};
use rustlavel_cache::{Cache, CacheStore};
use rustlavel_core::{Json, Result};
use rustlavel_db::row::Columns;
use rustlavel_db::{Database, Model, ModelExt, QueryBuilder, Row, Value};
use std::collections::BTreeMap;
use std::sync::Arc;
struct Entry {
sql: String,
generation: i64,
rows: Vec<Json>,
}
impl Entry {
fn to_json(&self) -> Json {
Json::object([
("sql", Json::from(self.sql.as_str())),
("g", Json::from(self.generation)),
("rows", Json::Array(self.rows.clone())),
])
}
fn from_json(value: &Json) -> Option<Entry> {
Some(Entry {
sql: value.get("sql")?.as_str()?.to_string(),
generation: value.get("g")?.as_i64()?,
rows: value.get("rows")?.as_array()?.to_vec(),
})
}
}
#[derive(Clone)]
pub struct ModelCache {
store: Arc<dyn Cache>,
regions: Arc<BTreeMap<&'static str, Region>>,
default_region: Arc<Region>,
stats: Arc<Stats>,
opt_in: bool,
}
impl ModelCache {
pub fn new(store: impl Cache) -> ModelCache {
ModelCache::from_arc(Arc::new(store))
}
pub fn shared(store: &CacheStore) -> ModelCache {
ModelCache::from_arc(store.driver_handle())
}
fn from_arc(store: Arc<dyn Cache>) -> ModelCache {
ModelCache {
store,
regions: Arc::new(BTreeMap::new()),
default_region: Arc::new(Region::default()),
stats: Arc::new(Stats::default()),
opt_in: true,
}
}
pub fn region<M: Model>(mut self, region: Region) -> ModelCache {
region.check(M::TABLE).expect("the cache region settings contradict each other");
Arc::make_mut(&mut self.regions).insert(M::TABLE, region);
self
}
pub fn cache_everything(mut self, region: Region) -> ModelCache {
region.check("<default>").expect("the default region settings contradict each other");
self.default_region = Arc::new(region);
self.opt_in = false;
self
}
pub fn stats(&self) -> &Stats {
&self.stats
}
fn region_for(&self, table: &str) -> Option<&Region> {
match self.regions.get(table) {
Some(region) => Some(region),
None if self.opt_in => None,
None => Some(&self.default_region),
}
}
pub async fn find<M: Model + Send>(&self, db: &Database, key: M::Key) -> Result<Option<M>> {
let Some(region) = self.region_for(M::TABLE).filter(|r| r.entities) else {
return M::find(db, key).await;
};
let text = Json::from(key.clone().into()).to_string();
let cache_key = keys::entity(M::TABLE, &text);
if let Some(model) = self
.store
.get(&cache_key)
.await?
.as_ref()
.and_then(row_from_json)
.and_then(|row| M::from_row(&row).ok())
{
self.stats.record(M::TABLE, Event::EntityHit);
return Ok(Some(model));
}
self.stats.record(M::TABLE, Event::EntityMiss);
let Some(row) = self.select_one(db, M::TABLE, M::PRIMARY_KEY, key.into()).await? else {
return Ok(None);
};
let model = M::from_row(&row)?;
if let Some(json) = row_to_json(&row) {
self.put(&cache_key, json, region).await?;
}
Ok(Some(model))
}
pub async fn get<M: Model + Send>(&self, db: &Database, query: QueryBuilder) -> Result<Vec<M>> {
let Some(region) = self.region_for(M::TABLE).filter(|r| r.queries) else {
return M::get(db, query).await;
};
let (sql, bindings) = query.to_sql(db.dialect())?;
let cache_key = keys::query(M::TABLE, keys::fingerprint(&sql, &bindings));
let generation = self.generation(M::TABLE, region).await?;
let usable = self
.store
.get(&cache_key)
.await?
.as_ref()
.and_then(Entry::from_json)
.filter(|entry| entry.sql == sql && entry.generation == generation);
if let Some(models) = usable.and_then(|entry| {
entry
.rows
.iter()
.map(|value| row_from_json(value).and_then(|row| M::from_row(&row).ok()))
.collect::<Option<Vec<M>>>()
}) {
self.stats.record(M::TABLE, Event::QueryHit);
return Ok(models);
}
self.stats.record(M::TABLE, Event::QueryMiss);
let rows = query.get(db).await?;
let models = M::hydrate(&rows)?;
if rows.len() > region.max_rows {
self.stats.record(M::TABLE, Event::TooLarge);
return Ok(models);
}
let Some(encoded) = rows.iter().map(row_to_json).collect::<Option<Vec<Json>>>() else {
self.stats.record(M::TABLE, Event::Unsupported);
return Ok(models);
};
let entry = Entry { sql, generation, rows: encoded };
self.put(&cache_key, entry.to_json(), region).await?;
Ok(models)
}
pub async fn first<M: Model + Send>(
&self,
db: &Database,
query: QueryBuilder,
) -> Result<Option<M>> {
Ok(self.get::<M>(db, query.limit(1)).await?.into_iter().next())
}
pub async fn insert<M: Model + Send + Sync>(&self, db: &Database, model: &mut M) -> Result<()> {
model.insert(db).await?;
self.invalidate_table::<M>().await
}
pub async fn update<M: Model + Send + Sync>(&self, db: &Database, model: &M) -> Result<u64> {
let affected = model.update(db).await?;
self.forget::<M>(model.key()).await?;
self.invalidate_table::<M>().await?;
Ok(affected)
}
pub async fn delete<M: Model + Send + Sync>(&self, db: &Database, model: &M) -> Result<u64> {
let affected = model.delete(db).await?;
self.forget::<M>(model.key()).await?;
self.invalidate_table::<M>().await?;
Ok(affected)
}
pub async fn forget<M: Model>(&self, key: M::Key) -> Result<()> {
let text = Json::from(key.into()).to_string();
self.store.forget(&keys::entity(M::TABLE, &text)).await?;
Ok(())
}
pub async fn invalidate_table<M: Model>(&self) -> Result<()> {
let Some(region) = self.region_for(M::TABLE) else { return Ok(()) };
if region.strategy == Strategy::ReadOnly {
return Ok(());
}
self.store.increment(&keys::generation(M::TABLE), 1).await?;
self.stats.record(M::TABLE, Event::Invalidated);
Ok(())
}
pub async fn invalidate_all(&self) -> Result<()> {
for (table, region) in self.regions.iter() {
if region.strategy == Strategy::ReadOnly {
continue;
}
self.store.increment(&keys::generation(table), 1).await?;
self.stats.record(table, Event::Invalidated);
}
Ok(())
}
async fn generation(&self, table: &str, region: &Region) -> Result<i64> {
if region.strategy == Strategy::ReadOnly {
return Ok(0);
}
Ok(self.store.get(&keys::generation(table)).await?.and_then(|v| v.as_i64()).unwrap_or(0))
}
async fn put(&self, key: &str, value: Json, region: &Region) -> Result<()> {
match region.ttl {
Some(ttl) => self.store.put(key, value, ttl).await,
None => self.store.forever(key, value).await,
}
}
async fn select_one(
&self,
db: &Database,
table: &str,
primary_key: &str,
key: Value,
) -> Result<Option<Row>> {
let rows = db.table(table).filter(primary_key, key).limit(1).get(db).await?;
Ok(rows.into_iter().next())
}
}
impl std::fmt::Debug for ModelCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModelCache")
.field("driver", &self.store.driver())
.field("regions", &self.regions.keys().collect::<Vec<_>>())
.field("opt_in", &self.opt_in)
.finish()
}
}
fn row_to_json(row: &Row) -> Option<Json> {
let mut fields = Vec::with_capacity(row.columns().len());
for name in row.columns() {
let value = row.value(name).ok()?;
fields.push((name.clone(), value_to_json(value)?));
}
Some(Json::object(fields))
}
fn row_from_json(value: &Json) -> Option<Row> {
let Json::Object(map) = value else { return None };
let columns: Vec<String> = map.keys().cloned().collect();
let values: Vec<Value> = map.values().map(json_to_value).collect();
Some(Row::new(Columns::new(columns), values))
}
fn value_to_json(value: &Value) -> Option<Json> {
match value {
Value::Bytes(_) => None,
other => Some(Json::from(other.clone())),
}
}
fn json_to_value(value: &Json) -> Value {
match value {
Json::Null => Value::Null,
Json::Bool(b) => Value::Bool(*b),
Json::Number(n) if n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 => {
Value::Int(*n as i64)
}
Json::Number(n) => Value::Float(*n),
Json::String(s) => Value::Text(s.clone()),
other => Value::Json(other.clone()),
}
}
impl ModelCache {
pub fn store(&self) -> &Arc<dyn Cache> {
&self.store
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row(pairs: &[(&str, Value)]) -> Row {
let columns: Vec<String> = pairs.iter().map(|(n, _)| (*n).to_string()).collect();
let values: Vec<Value> = pairs.iter().map(|(_, v)| v.clone()).collect();
Row::new(Columns::new(columns), values)
}
#[test]
fn a_row_survives_the_round_trip_through_json() {
let original = row(&[
("id", Value::from(7i64)),
("name", Value::from("Ada Lovelace")),
("verified", Value::from(true)),
("deleted_at", Value::Null),
]);
let back = row_from_json(&row_to_json(&original).expect("not encodable")).expect("did not parse");
assert_eq!(back.get::<i64>("id").unwrap(), 7);
assert_eq!(back.get::<String>("name").unwrap(), "Ada Lovelace");
assert!(back.get::<bool>("verified").unwrap());
assert_eq!(back.columns().len(), 4, "a column was lost");
}
#[test]
fn a_row_with_a_binary_column_is_not_cached_at_all() {
let with_blob = row(&[("id", Value::from(1i64)), ("avatar", Value::Bytes(vec![1, 2, 3]))]);
assert!(row_to_json(&with_blob).is_none(), "a blob was silently turned into text");
}
#[test]
fn numbers_come_back_as_the_variant_the_column_needs() {
assert_eq!(json_to_value(&Json::Number(7.0)), Value::Int(7));
assert_eq!(json_to_value(&Json::Number(-7.0)), Value::Int(-7));
assert_eq!(json_to_value(&Json::Number(7.5)), Value::Float(7.5));
assert_eq!(json_to_value(&Json::Null), Value::Null);
assert_eq!(json_to_value(&Json::Bool(true)), Value::Bool(true));
assert_eq!(json_to_value(&Json::from("x")), Value::Text("x".into()));
}
#[test]
fn rubbish_in_the_cache_reads_as_a_miss_rather_than_an_error() {
assert!(row_from_json(&Json::from("not an object")).is_none());
assert!(row_from_json(&Json::Null).is_none());
assert!(row_from_json(&Json::Array(vec![])).is_none());
}
#[test]
fn a_query_entry_carries_the_statement_it_was_taken_from() {
let entry = Entry {
sql: "select * from users where role = ?".into(),
generation: 3,
rows: vec![Json::object([("id", Json::from(1))])],
};
let back = Entry::from_json(&entry.to_json()).expect("did not parse");
assert_eq!(back.sql, entry.sql);
assert_eq!(back.generation, 3);
assert_eq!(back.rows.len(), 1);
assert!(Entry::from_json(&Json::from("nonsense")).is_none());
assert!(Entry::from_json(&Json::object([("sql", Json::from("x"))])).is_none());
}
}