1use crate::keys;
4use crate::region::{Region, Strategy};
5use crate::stats::{Event, Stats};
6use rustlavel_cache::{Cache, CacheStore};
7use rustlavel_core::{Json, Result};
8use rustlavel_db::row::Columns;
9use rustlavel_db::{Database, Model, ModelExt, QueryBuilder, Row, Value};
10use std::collections::BTreeMap;
11use std::sync::Arc;
12
13struct Entry {
20 sql: String,
21 generation: i64,
22 rows: Vec<Json>,
23}
24
25impl Entry {
26 fn to_json(&self) -> Json {
27 Json::object([
28 ("sql", Json::from(self.sql.as_str())),
29 ("g", Json::from(self.generation)),
30 ("rows", Json::Array(self.rows.clone())),
31 ])
32 }
33
34 fn from_json(value: &Json) -> Option<Entry> {
35 Some(Entry {
36 sql: value.get("sql")?.as_str()?.to_string(),
37 generation: value.get("g")?.as_i64()?,
38 rows: value.get("rows")?.as_array()?.to_vec(),
39 })
40 }
41}
42
43#[derive(Clone)]
47pub struct ModelCache {
48 store: Arc<dyn Cache>,
49 regions: Arc<BTreeMap<&'static str, Region>>,
50 default_region: Arc<Region>,
51 stats: Arc<Stats>,
52 opt_in: bool,
58}
59
60impl ModelCache {
61 pub fn new(store: impl Cache) -> ModelCache {
63 ModelCache::from_arc(Arc::new(store))
64 }
65
66 pub fn shared(store: &CacheStore) -> ModelCache {
68 ModelCache::from_arc(store.driver_handle())
69 }
70
71 fn from_arc(store: Arc<dyn Cache>) -> ModelCache {
72 ModelCache {
73 store,
74 regions: Arc::new(BTreeMap::new()),
75 default_region: Arc::new(Region::default()),
76 stats: Arc::new(Stats::default()),
77 opt_in: true,
78 }
79 }
80
81 pub fn region<M: Model>(mut self, region: Region) -> ModelCache {
87 region.check(M::TABLE).expect("the cache region settings contradict each other");
88 Arc::make_mut(&mut self.regions).insert(M::TABLE, region);
89 self
90 }
91
92 pub fn cache_everything(mut self, region: Region) -> ModelCache {
97 region.check("<default>").expect("the default region settings contradict each other");
98 self.default_region = Arc::new(region);
99 self.opt_in = false;
100 self
101 }
102
103 pub fn stats(&self) -> &Stats {
104 &self.stats
105 }
106
107 fn region_for(&self, table: &str) -> Option<&Region> {
109 match self.regions.get(table) {
110 Some(region) => Some(region),
111 None if self.opt_in => None,
112 None => Some(&self.default_region),
113 }
114 }
115
116 pub async fn find<M: Model + Send>(&self, db: &Database, key: M::Key) -> Result<Option<M>> {
120 let Some(region) = self.region_for(M::TABLE).filter(|r| r.entities) else {
121 return M::find(db, key).await;
122 };
123
124 let text = Json::from(key.clone().into()).to_string();
125 let cache_key = keys::entity(M::TABLE, &text);
126
127 if let Some(cached) = self.store.get(&cache_key).await? {
128 self.stats.record(M::TABLE, Event::EntityHit);
129 if let Some(model) = row_from_json(&cached).and_then(|row| M::from_row(&row).ok()) {
134 return Ok(Some(model));
135 }
136 } else {
137 self.stats.record(M::TABLE, Event::EntityMiss);
138 }
139
140 let found = M::find(db, key).await?;
141 if let Some(model) = &found {
142 let row = self.select_one(db, M::TABLE, M::PRIMARY_KEY, model.key().into()).await?;
143 if let Some(json) = row.as_ref().and_then(row_to_json) {
144 self.put(&cache_key, json, region).await?;
145 }
146 }
147 Ok(found)
148 }
149
150 pub async fn get<M: Model + Send>(&self, db: &Database, query: QueryBuilder) -> Result<Vec<M>> {
152 let Some(region) = self.region_for(M::TABLE).filter(|r| r.queries) else {
153 return M::get(db, query).await;
154 };
155
156 let (sql, bindings) = query.to_sql(db.dialect())?;
157 let cache_key = keys::query(M::TABLE, keys::fingerprint(&sql, &bindings));
158 let generation = self.generation(M::TABLE, region).await?;
159
160 let usable = self
165 .store
166 .get(&cache_key)
167 .await?
168 .as_ref()
169 .and_then(Entry::from_json)
170 .filter(|entry| entry.sql == sql && entry.generation == generation);
171
172 if let Some(models) = usable.and_then(|entry| {
176 entry
177 .rows
178 .iter()
179 .map(|value| row_from_json(value).and_then(|row| M::from_row(&row).ok()))
180 .collect::<Option<Vec<M>>>()
181 }) {
182 self.stats.record(M::TABLE, Event::QueryHit);
183 return Ok(models);
184 }
185 self.stats.record(M::TABLE, Event::QueryMiss);
186
187 let rows = query.get(db).await?;
188 let models = M::hydrate(&rows)?;
189
190 if rows.len() > region.max_rows {
191 self.stats.record(M::TABLE, Event::TooLarge);
192 return Ok(models);
193 }
194
195 let Some(encoded) = rows.iter().map(row_to_json).collect::<Option<Vec<Json>>>() else {
198 self.stats.record(M::TABLE, Event::Unsupported);
199 return Ok(models);
200 };
201
202 let entry = Entry { sql, generation, rows: encoded };
203 self.put(&cache_key, entry.to_json(), region).await?;
204 Ok(models)
205 }
206
207 pub async fn first<M: Model + Send>(
209 &self,
210 db: &Database,
211 query: QueryBuilder,
212 ) -> Result<Option<M>> {
213 Ok(self.get::<M>(db, query.limit(1)).await?.into_iter().next())
214 }
215
216 pub async fn insert<M: Model + Send + Sync>(&self, db: &Database, model: &mut M) -> Result<()> {
220 model.insert(db).await?;
221 self.invalidate_table::<M>().await
222 }
223
224 pub async fn update<M: Model + Send + Sync>(&self, db: &Database, model: &M) -> Result<u64> {
226 let affected = model.update(db).await?;
227 self.forget::<M>(model.key()).await?;
228 self.invalidate_table::<M>().await?;
229 Ok(affected)
230 }
231
232 pub async fn delete<M: Model + Send + Sync>(&self, db: &Database, model: &M) -> Result<u64> {
234 let affected = model.delete(db).await?;
235 self.forget::<M>(model.key()).await?;
236 self.invalidate_table::<M>().await?;
237 Ok(affected)
238 }
239
240 pub async fn forget<M: Model>(&self, key: M::Key) -> Result<()> {
246 let text = Json::from(key.into()).to_string();
247 self.store.forget(&keys::entity(M::TABLE, &text)).await?;
248 Ok(())
249 }
250
251 pub async fn invalidate_table<M: Model>(&self) -> Result<()> {
258 let Some(region) = self.region_for(M::TABLE) else { return Ok(()) };
259 if region.strategy == Strategy::ReadOnly {
260 return Ok(());
263 }
264 self.store.increment(&keys::generation(M::TABLE), 1).await?;
265 self.stats.record(M::TABLE, Event::Invalidated);
266 Ok(())
267 }
268
269 pub async fn invalidate_all(&self) -> Result<()> {
275 for (table, region) in self.regions.iter() {
276 if region.strategy == Strategy::ReadOnly {
277 continue;
278 }
279 self.store.increment(&keys::generation(table), 1).await?;
280 self.stats.record(table, Event::Invalidated);
281 }
282 Ok(())
283 }
284
285 async fn generation(&self, table: &str, region: &Region) -> Result<i64> {
292 if region.strategy == Strategy::ReadOnly {
293 return Ok(0);
294 }
295 Ok(self.store.get(&keys::generation(table)).await?.and_then(|v| v.as_i64()).unwrap_or(0))
296 }
297
298 async fn put(&self, key: &str, value: Json, region: &Region) -> Result<()> {
299 match region.ttl {
300 Some(ttl) => self.store.put(key, value, ttl).await,
301 None => self.store.forever(key, value).await,
302 }
303 }
304
305 async fn select_one(
313 &self,
314 db: &Database,
315 table: &str,
316 primary_key: &str,
317 key: Value,
318 ) -> Result<Option<Row>> {
319 let rows = db.table(table).filter(primary_key, key).limit(1).get(db).await?;
320 Ok(rows.into_iter().next())
321 }
322}
323
324impl std::fmt::Debug for ModelCache {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 f.debug_struct("ModelCache")
327 .field("driver", &self.store.driver())
328 .field("regions", &self.regions.keys().collect::<Vec<_>>())
329 .field("opt_in", &self.opt_in)
330 .finish()
331 }
332}
333
334fn row_to_json(row: &Row) -> Option<Json> {
340 let mut fields = Vec::with_capacity(row.columns().len());
341 for name in row.columns() {
342 let value = row.value(name).ok()?;
343 fields.push((name.clone(), value_to_json(value)?));
344 }
345 Some(Json::object(fields))
346}
347
348fn row_from_json(value: &Json) -> Option<Row> {
351 let Json::Object(map) = value else { return None };
352 let columns: Vec<String> = map.keys().cloned().collect();
353 let values: Vec<Value> = map.values().map(json_to_value).collect();
354 Some(Row::new(Columns::new(columns), values))
355}
356
357fn value_to_json(value: &Value) -> Option<Json> {
366 match value {
367 Value::Bytes(_) => None,
368 other => Some(Json::from(other.clone())),
369 }
370}
371
372fn json_to_value(value: &Json) -> Value {
380 match value {
381 Json::Null => Value::Null,
382 Json::Bool(b) => Value::Bool(*b),
383 Json::Number(n) if n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 => {
384 Value::Int(*n as i64)
385 }
386 Json::Number(n) => Value::Float(*n),
387 Json::String(s) => Value::Text(s.clone()),
388 other => Value::Json(other.clone()),
389 }
390}
391
392impl ModelCache {
394 pub fn store(&self) -> &Arc<dyn Cache> {
395 &self.store
396 }
397}
398
399#[cfg(test)]
401mod tests {
402 use super::*;
403
404 fn row(pairs: &[(&str, Value)]) -> Row {
405 let columns: Vec<String> = pairs.iter().map(|(n, _)| (*n).to_string()).collect();
406 let values: Vec<Value> = pairs.iter().map(|(_, v)| v.clone()).collect();
407 Row::new(Columns::new(columns), values)
408 }
409
410 #[test]
411 fn a_row_survives_the_round_trip_through_json() {
412 let original = row(&[
413 ("id", Value::from(7i64)),
414 ("name", Value::from("Ada Lovelace")),
415 ("verified", Value::from(true)),
416 ("deleted_at", Value::Null),
417 ]);
418
419 let back = row_from_json(&row_to_json(&original).expect("not encodable")).expect("did not parse");
420
421 assert_eq!(back.get::<i64>("id").unwrap(), 7);
422 assert_eq!(back.get::<String>("name").unwrap(), "Ada Lovelace");
423 assert!(back.get::<bool>("verified").unwrap());
424 assert_eq!(back.columns().len(), 4, "a column was lost");
425 }
426
427 #[test]
430 fn a_row_with_a_binary_column_is_not_cached_at_all() {
431 let with_blob = row(&[("id", Value::from(1i64)), ("avatar", Value::Bytes(vec![1, 2, 3]))]);
432
433 assert!(row_to_json(&with_blob).is_none(), "a blob was silently turned into text");
434 }
435
436 #[test]
439 fn numbers_come_back_as_the_variant_the_column_needs() {
440 assert_eq!(json_to_value(&Json::Number(7.0)), Value::Int(7));
441 assert_eq!(json_to_value(&Json::Number(-7.0)), Value::Int(-7));
442 assert_eq!(json_to_value(&Json::Number(7.5)), Value::Float(7.5));
443 assert_eq!(json_to_value(&Json::Null), Value::Null);
444 assert_eq!(json_to_value(&Json::Bool(true)), Value::Bool(true));
445 assert_eq!(json_to_value(&Json::from("x")), Value::Text("x".into()));
446 }
447
448 #[test]
451 fn rubbish_in_the_cache_reads_as_a_miss_rather_than_an_error() {
452 assert!(row_from_json(&Json::from("not an object")).is_none());
453 assert!(row_from_json(&Json::Null).is_none());
454 assert!(row_from_json(&Json::Array(vec![])).is_none());
455 }
456
457 #[test]
458 fn a_query_entry_carries_the_statement_it_was_taken_from() {
459 let entry = Entry {
460 sql: "select * from users where role = ?".into(),
461 generation: 3,
462 rows: vec![Json::object([("id", Json::from(1))])],
463 };
464
465 let back = Entry::from_json(&entry.to_json()).expect("did not parse");
466 assert_eq!(back.sql, entry.sql);
467 assert_eq!(back.generation, 3);
468 assert_eq!(back.rows.len(), 1);
469
470 assert!(Entry::from_json(&Json::from("nonsense")).is_none());
472 assert!(Entry::from_json(&Json::object([("sql", Json::from("x"))])).is_none());
473 }
474}