oracle_rs/
statement_cache.rs1use indexmap::IndexMap;
18use std::time::Instant;
19
20use crate::statement::Statement;
21
22#[derive(Debug)]
24struct CachedStatement {
25 statement: Statement,
27 in_use: bool,
29 last_used: Instant,
31}
32
33impl CachedStatement {
34 fn new(statement: Statement) -> Self {
35 Self {
36 statement,
37 in_use: false,
38 last_used: Instant::now(),
39 }
40 }
41
42 fn touch(&mut self) {
43 self.last_used = Instant::now();
44 }
45}
46
47#[derive(Debug)]
69pub struct StatementCache {
70 cache: IndexMap<String, CachedStatement>,
72 max_size: usize,
74}
75
76impl StatementCache {
77 pub fn new(max_size: usize) -> Self {
81 Self {
82 cache: IndexMap::with_capacity(max_size),
83 max_size,
84 }
85 }
86
87 pub fn get(&mut self, sql: &str) -> Option<Statement> {
93 if self.max_size == 0 {
94 return None;
95 }
96
97 if let Some(cached) = self.cache.get_mut(sql) {
99 cached.touch();
100
101 if cached.in_use {
102 tracing::trace!(sql = sql, "Statement cache hit but in use, returning fresh");
105 return None;
106 }
107
108 cached.in_use = true;
110 tracing::trace!(
111 sql = sql,
112 cursor_id = cached.statement.cursor_id(),
113 "Statement cache hit"
114 );
115 return Some(cached.statement.clone_for_reuse());
116 }
117
118 tracing::trace!(sql = sql, "Statement cache miss");
119 None
120 }
121
122 pub fn put(&mut self, sql: String, statement: Statement) {
127 if self.max_size == 0 {
128 return;
129 }
130
131 if statement.is_ddl() {
133 tracing::trace!(sql = sql, "Not caching DDL statement");
134 return;
135 }
136
137 if statement.cursor_id() == 0 {
139 tracing::trace!(sql = sql, "Not caching statement without cursor_id");
140 return;
141 }
142
143 if let Some(cached) = self.cache.get_mut(&sql) {
145 cached.statement = statement;
146 cached.in_use = false;
147 cached.touch();
148 tracing::trace!(sql = sql, "Updated existing cache entry");
149 return;
150 }
151
152 if self.cache.len() >= self.max_size {
154 self.evict_lru();
155 }
156
157 tracing::trace!(
158 sql = sql,
159 cursor_id = statement.cursor_id(),
160 "Adding statement to cache"
161 );
162 self.cache.insert(sql, CachedStatement::new(statement));
163 }
164
165 pub fn return_statement(&mut self, sql: &str) {
169 if let Some(cached) = self.cache.get_mut(sql) {
170 cached.in_use = false;
171 tracing::trace!(sql = sql, "Statement returned to cache");
172 }
173 }
174
175 pub fn mark_cursor_closed(&mut self, sql: &str) {
182 if let Some(cached) = self.cache.get_mut(sql) {
183 if cached.statement.cursor_id() != 0 {
184 cached.statement.set_cursor_id(0);
185 cached.statement.set_executed(false);
186 tracing::trace!(sql = sql, "Cursor closed, reset cursor_id to 0");
187 }
188 }
189 }
190
191 pub fn clear(&mut self) {
195 self.cache.clear();
196 tracing::debug!("Statement cache cleared");
197 }
198
199 pub fn len(&self) -> usize {
201 self.cache.len()
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.cache.is_empty()
207 }
208
209 pub fn max_size(&self) -> usize {
211 self.max_size
212 }
213
214 fn evict_lru(&mut self) {
216 let lru_key = self
218 .cache
219 .iter()
220 .filter(|(_, cached)| !cached.in_use)
221 .min_by_key(|(_, cached)| cached.last_used)
222 .map(|(key, _)| key.clone());
223
224 if let Some(key) = lru_key {
225 if let Some(cached) = self.cache.swap_remove(&key) {
226 tracing::trace!(
227 sql = key,
228 cursor_id = cached.statement.cursor_id(),
229 "Evicted LRU statement from cache"
230 );
231 }
232 } else {
233 tracing::warn!("Statement cache full and all statements in use");
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn make_test_statement(sql: &str, cursor_id: u16) -> Statement {
244 let mut stmt = Statement::new(sql);
245 stmt.set_cursor_id(cursor_id);
246 stmt.set_executed(true);
247 stmt
248 }
249
250 #[test]
251 fn test_cache_basic() {
252 let mut cache = StatementCache::new(5);
253
254 let stmt = make_test_statement("SELECT 1 FROM DUAL", 100);
256 cache.put("SELECT 1 FROM DUAL".to_string(), stmt);
257
258 assert_eq!(cache.len(), 1);
259
260 let cached = cache.get("SELECT 1 FROM DUAL").expect("Should be cached");
262 assert_eq!(cached.cursor_id(), 100);
263
264 cache.return_statement("SELECT 1 FROM DUAL");
266 }
267
268 #[test]
269 fn test_cache_miss() {
270 let mut cache = StatementCache::new(5);
271 assert!(cache.get("SELECT 1 FROM DUAL").is_none());
272 }
273
274 #[test]
275 fn test_cache_disabled() {
276 let mut cache = StatementCache::new(0);
277
278 let stmt = make_test_statement("SELECT 1 FROM DUAL", 100);
279 cache.put("SELECT 1 FROM DUAL".to_string(), stmt);
280
281 assert_eq!(cache.len(), 0);
282 assert!(cache.get("SELECT 1 FROM DUAL").is_none());
283 }
284
285 #[test]
286 fn test_ddl_not_cached() {
287 let mut cache = StatementCache::new(5);
288
289 let mut stmt = Statement::new("CREATE TABLE test (id NUMBER)");
290 stmt.set_cursor_id(100);
291 cache.put("CREATE TABLE test (id NUMBER)".to_string(), stmt);
292
293 assert_eq!(cache.len(), 0);
294 }
295
296 #[test]
297 fn test_no_cursor_not_cached() {
298 let mut cache = StatementCache::new(5);
299
300 let stmt = Statement::new("SELECT 1 FROM DUAL");
302 cache.put("SELECT 1 FROM DUAL".to_string(), stmt);
303
304 assert_eq!(cache.len(), 0);
305 }
306
307 #[test]
308 fn test_lru_eviction() {
309 let mut cache = StatementCache::new(3);
310
311 cache.put(
313 "SELECT 1 FROM DUAL".to_string(),
314 make_test_statement("SELECT 1 FROM DUAL", 1),
315 );
316 cache.put(
317 "SELECT 2 FROM DUAL".to_string(),
318 make_test_statement("SELECT 2 FROM DUAL", 2),
319 );
320 cache.put(
321 "SELECT 3 FROM DUAL".to_string(),
322 make_test_statement("SELECT 3 FROM DUAL", 3),
323 );
324
325 assert_eq!(cache.len(), 3);
326
327 cache.get("SELECT 1 FROM DUAL");
329 cache.return_statement("SELECT 1 FROM DUAL");
330
331 cache.put(
333 "SELECT 4 FROM DUAL".to_string(),
334 make_test_statement("SELECT 4 FROM DUAL", 4),
335 );
336
337 assert_eq!(cache.len(), 3);
338 assert!(cache.get("SELECT 2 FROM DUAL").is_none()); assert!(cache.get("SELECT 1 FROM DUAL").is_some()); }
341
342 #[test]
343 fn test_in_use_not_returned() {
344 let mut cache = StatementCache::new(5);
345
346 cache.put(
347 "SELECT 1 FROM DUAL".to_string(),
348 make_test_statement("SELECT 1 FROM DUAL", 100),
349 );
350
351 let _ = cache.get("SELECT 1 FROM DUAL");
353
354 assert!(cache.get("SELECT 1 FROM DUAL").is_none());
356
357 cache.return_statement("SELECT 1 FROM DUAL");
359
360 assert!(cache.get("SELECT 1 FROM DUAL").is_some());
362 }
363
364 #[test]
365 fn test_clear() {
366 let mut cache = StatementCache::new(5);
367
368 cache.put(
369 "SELECT 1 FROM DUAL".to_string(),
370 make_test_statement("SELECT 1 FROM DUAL", 1),
371 );
372 cache.put(
373 "SELECT 2 FROM DUAL".to_string(),
374 make_test_statement("SELECT 2 FROM DUAL", 2),
375 );
376
377 assert_eq!(cache.len(), 2);
378
379 cache.clear();
380
381 assert_eq!(cache.len(), 0);
382 }
383
384 #[test]
385 fn test_update_existing() {
386 let mut cache = StatementCache::new(5);
387
388 cache.put(
389 "SELECT 1 FROM DUAL".to_string(),
390 make_test_statement("SELECT 1 FROM DUAL", 100),
391 );
392
393 cache.put(
395 "SELECT 1 FROM DUAL".to_string(),
396 make_test_statement("SELECT 1 FROM DUAL", 200),
397 );
398
399 assert_eq!(cache.len(), 1);
400
401 let cached = cache.get("SELECT 1 FROM DUAL").unwrap();
402 assert_eq!(cached.cursor_id(), 200);
403 }
404}