rustrails_record/
query_cache.rs1use std::cell::RefCell;
2use std::sync::Arc;
3
4use rustrails_support::cache::{CacheOptions, CacheStore, MemoryStore};
5use serde_json::Value;
6
7thread_local! {
8 static QUERY_CACHE_SCOPE: RefCell<Vec<QueryCacheScope>> = const { RefCell::new(Vec::new()) };
9}
10
11#[derive(Clone)]
12struct QueryCacheScope {
13 cache: QueryCache,
14 bypass_depth: usize,
15}
16
17#[derive(Clone, Debug)]
19pub struct QueryCache {
20 store: Arc<MemoryStore>,
21}
22
23impl QueryCache {
24 #[must_use]
26 pub fn new() -> Self {
27 Self {
28 store: Arc::new(MemoryStore::new()),
29 }
30 }
31
32 #[must_use]
34 pub fn cache_key(sql: &str, binds: &[Value]) -> String {
35 let binds = match serde_json::to_string(binds) {
36 Ok(serialized) => serialized,
37 Err(_) => "[]".to_owned(),
38 };
39 format!("{sql}::{binds}")
40 }
41
42 pub fn with_request_scope<F, R>(&self, f: F) -> R
44 where
45 F: FnOnce() -> R,
46 {
47 QUERY_CACHE_SCOPE.with(|scope| {
48 scope.borrow_mut().push(QueryCacheScope {
49 cache: QueryCache::new(),
50 bypass_depth: 0,
51 });
52 });
53
54 struct Guard;
55 impl Drop for Guard {
56 fn drop(&mut self) {
57 QUERY_CACHE_SCOPE.with(|scope| {
58 scope.borrow_mut().pop();
59 });
60 }
61 }
62
63 let _guard = Guard;
64 f()
65 }
66
67 pub fn fetch<F>(&self, sql: &str, binds: &[Value], loader: F) -> Value
69 where
70 F: FnOnce() -> Value,
71 {
72 let key = Self::cache_key(sql, binds);
73
74 if let Some((cache, bypassed)) = current_scope() {
75 if bypassed {
76 return loader();
77 }
78 return cache.store.fetch(&key, CacheOptions::default(), loader);
79 }
80
81 self.store.fetch(&key, CacheOptions::default(), loader)
82 }
83
84 pub fn execute_write<F, R>(&self, operation: F) -> R
86 where
87 F: FnOnce() -> R,
88 {
89 let result = operation();
90 if let Some((cache, _)) = current_scope() {
91 cache.store.clear();
92 } else {
93 self.store.clear();
94 }
95 result
96 }
97
98 pub fn uncached<F, R>(f: F) -> R
100 where
101 F: FnOnce() -> R,
102 {
103 QUERY_CACHE_SCOPE.with(|scope| {
104 if let Some(current) = scope.borrow_mut().last_mut() {
105 current.bypass_depth += 1;
106 }
107 });
108
109 struct Guard;
110 impl Drop for Guard {
111 fn drop(&mut self) {
112 QUERY_CACHE_SCOPE.with(|scope| {
113 if let Some(current) = scope.borrow_mut().last_mut() {
114 current.bypass_depth = current.bypass_depth.saturating_sub(1);
115 }
116 });
117 }
118 }
119
120 let _guard = Guard;
121 f()
122 }
123}
124
125impl Default for QueryCache {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131fn current_scope() -> Option<(QueryCache, bool)> {
132 QUERY_CACHE_SCOPE.with(|scope| {
133 scope
134 .borrow()
135 .last()
136 .map(|scope| (scope.cache.clone(), scope.bypass_depth > 0))
137 })
138}
139
140#[cfg(test)]
141mod tests {
142 use std::sync::atomic::{AtomicUsize, Ordering};
143
144 use serde_json::json;
145
146 use super::QueryCache;
147
148 #[test]
149 fn cache_key_depends_on_sql_and_binds() {
150 let first = QueryCache::cache_key("SELECT 1", &[json!(1)]);
151 let second = QueryCache::cache_key("SELECT 1", &[json!(2)]);
152 assert_ne!(first, second);
153 }
154
155 #[test]
156 fn fetch_uses_cached_value_within_request_scope() {
157 let cache = QueryCache::new();
158 let calls = AtomicUsize::new(0);
159
160 cache.with_request_scope(|| {
161 let first = cache.fetch("SELECT * FROM users WHERE id = ?", &[json!(1)], || {
162 calls.fetch_add(1, Ordering::SeqCst);
163 json!({"id": 1})
164 });
165 let second = cache.fetch("SELECT * FROM users WHERE id = ?", &[json!(1)], || {
166 calls.fetch_add(1, Ordering::SeqCst);
167 json!({"id": 2})
168 });
169
170 assert_eq!(first, second);
171 });
172
173 assert_eq!(calls.load(Ordering::SeqCst), 1);
174 }
175
176 #[test]
177 fn request_scopes_are_isolated() {
178 let cache = QueryCache::new();
179 let calls = AtomicUsize::new(0);
180
181 cache.with_request_scope(|| {
182 let _ = cache.fetch("SELECT 1", &[], || {
183 calls.fetch_add(1, Ordering::SeqCst);
184 json!(1)
185 });
186 });
187 cache.with_request_scope(|| {
188 let _ = cache.fetch("SELECT 1", &[], || {
189 calls.fetch_add(1, Ordering::SeqCst);
190 json!(1)
191 });
192 });
193
194 assert_eq!(calls.load(Ordering::SeqCst), 2);
195 }
196
197 #[test]
198 fn uncached_bypasses_request_cache() {
199 let cache = QueryCache::new();
200 let calls = AtomicUsize::new(0);
201
202 cache.with_request_scope(|| {
203 let _ = cache.fetch("SELECT 1", &[], || {
204 calls.fetch_add(1, Ordering::SeqCst);
205 json!(1)
206 });
207 let _ = QueryCache::uncached(|| {
208 cache.fetch("SELECT 1", &[], || {
209 calls.fetch_add(1, Ordering::SeqCst);
210 json!(2)
211 })
212 });
213 });
214
215 assert_eq!(calls.load(Ordering::SeqCst), 2);
216 }
217
218 #[test]
219 fn execute_write_invalidates_current_scope() {
220 let cache = QueryCache::new();
221 let calls = AtomicUsize::new(0);
222
223 cache.with_request_scope(|| {
224 let _ = cache.fetch("SELECT 1", &[], || {
225 calls.fetch_add(1, Ordering::SeqCst);
226 json!(1)
227 });
228 cache.execute_write(|| ());
229 let _ = cache.fetch("SELECT 1", &[], || {
230 calls.fetch_add(1, Ordering::SeqCst);
231 json!(1)
232 });
233 });
234
235 assert_eq!(calls.load(Ordering::SeqCst), 2);
236 }
237
238 #[test]
239 fn execute_write_invalidates_non_scoped_cache() {
240 let cache = QueryCache::new();
241 let calls = AtomicUsize::new(0);
242
243 let _ = cache.fetch("SELECT 1", &[], || {
244 calls.fetch_add(1, Ordering::SeqCst);
245 json!(1)
246 });
247 cache.execute_write(|| ());
248 let _ = cache.fetch("SELECT 1", &[], || {
249 calls.fetch_add(1, Ordering::SeqCst);
250 json!(1)
251 });
252
253 assert_eq!(calls.load(Ordering::SeqCst), 2);
254 }
255}