oracle_rs/
statement_cache.rs1use indexmap::IndexMap;
9use std::time::Instant;
10
11use crate::statement::Statement;
12
13#[derive(Debug)]
15struct CachedStatement {
16 statement: Statement,
18 in_use: bool,
20 last_used: Instant,
22}
23
24impl CachedStatement {
25 fn new(statement: Statement) -> Self {
26 Self {
27 statement,
28 in_use: false,
29 last_used: Instant::now(),
30 }
31 }
32
33 fn touch(&mut self) {
34 self.last_used = Instant::now();
35 }
36}
37
38#[derive(Debug)]
60pub struct StatementCache {
61 cache: IndexMap<String, CachedStatement>,
63 max_size: usize,
65 cursors_to_close: Vec<u16>,
68}
69
70impl StatementCache {
71 pub fn new(max_size: usize) -> Self {
75 Self {
76 cache: IndexMap::with_capacity(max_size),
77 max_size,
78 cursors_to_close: Vec::new(),
79 }
80 }
81
82 pub fn get(&mut self, sql: &str) -> Option<Statement> {
88 if self.max_size == 0 {
89 return None;
90 }
91
92 if let Some(cached) = self.cache.get_mut(sql) {
94 cached.touch();
95
96 if cached.in_use {
97 tracing::trace!(sql = sql, "Statement cache hit but in use, returning fresh");
100 return None;
101 }
102
103 cached.in_use = true;
105 tracing::trace!(
106 sql = sql,
107 cursor_id = cached.statement.cursor_id(),
108 "Statement cache hit"
109 );
110 return Some(cached.statement.clone_for_reuse());
111 }
112
113 tracing::trace!(sql = sql, "Statement cache miss");
114 None
115 }
116
117 pub fn put(&mut self, sql: String, statement: Statement) {
122 if self.max_size == 0 {
123 return;
124 }
125
126 if statement.is_ddl() {
128 tracing::trace!(sql = sql, "Not caching DDL statement");
129 return;
130 }
131
132 if statement.cursor_id() == 0 {
134 tracing::trace!(sql = sql, "Not caching statement without cursor_id");
135 return;
136 }
137
138 if let Some(cached) = self.cache.get_mut(&sql) {
140 cached.statement = statement;
141 cached.in_use = false;
142 cached.touch();
143 tracing::trace!(sql = sql, "Updated existing cache entry");
144 return;
145 }
146
147 if self.cache.len() >= self.max_size {
149 self.evict_lru();
150 }
151
152 tracing::trace!(
153 sql = sql,
154 cursor_id = statement.cursor_id(),
155 "Adding statement to cache"
156 );
157 self.cache.insert(sql, CachedStatement::new(statement));
158 }
159
160 pub fn return_statement(&mut self, sql: &str) {
164 if let Some(cached) = self.cache.get_mut(sql) {
165 cached.in_use = false;
166 tracing::trace!(sql = sql, "Statement returned to cache");
167 }
168 }
169
170 pub fn take_cursors_to_close(&mut self) -> Vec<u16> {
174 std::mem::take(&mut self.cursors_to_close)
175 }
176
177 pub fn clear(&mut self) {
182 for (_, cached) in self.cache.drain(..) {
183 if cached.statement.cursor_id() != 0 {
184 self.cursors_to_close.push(cached.statement.cursor_id());
185 }
186 }
187 tracing::debug!(
188 cursors_to_close = self.cursors_to_close.len(),
189 "Statement cache cleared"
190 );
191 }
192
193 pub fn len(&self) -> usize {
195 self.cache.len()
196 }
197
198 pub fn is_empty(&self) -> bool {
200 self.cache.is_empty()
201 }
202
203 pub fn max_size(&self) -> usize {
205 self.max_size
206 }
207
208 fn evict_lru(&mut self) {
210 let lru_key = self
212 .cache
213 .iter()
214 .filter(|(_, cached)| !cached.in_use)
215 .min_by_key(|(_, cached)| cached.last_used)
216 .map(|(key, _)| key.clone());
217
218 if let Some(key) = lru_key {
219 if let Some(cached) = self.cache.swap_remove(&key) {
220 if cached.statement.cursor_id() != 0 {
222 self.cursors_to_close.push(cached.statement.cursor_id());
223 }
224 tracing::trace!(
225 sql = key,
226 cursor_id = cached.statement.cursor_id(),
227 "Evicted LRU statement from cache"
228 );
229 }
230 } else {
231 tracing::warn!("Statement cache full and all statements in use");
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn make_test_statement(sql: &str, cursor_id: u16) -> Statement {
242 let mut stmt = Statement::new(sql);
243 stmt.set_cursor_id(cursor_id);
244 stmt.set_executed(true);
245 stmt
246 }
247
248 #[test]
249 fn test_cache_basic() {
250 let mut cache = StatementCache::new(5);
251
252 let stmt = make_test_statement("SELECT 1 FROM DUAL", 100);
254 cache.put("SELECT 1 FROM DUAL".to_string(), stmt);
255
256 assert_eq!(cache.len(), 1);
257
258 let cached = cache.get("SELECT 1 FROM DUAL").expect("Should be cached");
260 assert_eq!(cached.cursor_id(), 100);
261
262 cache.return_statement("SELECT 1 FROM DUAL");
264 }
265
266 #[test]
267 fn test_cache_miss() {
268 let mut cache = StatementCache::new(5);
269 assert!(cache.get("SELECT 1 FROM DUAL").is_none());
270 }
271
272 #[test]
273 fn test_cache_disabled() {
274 let mut cache = StatementCache::new(0);
275
276 let stmt = make_test_statement("SELECT 1 FROM DUAL", 100);
277 cache.put("SELECT 1 FROM DUAL".to_string(), stmt);
278
279 assert_eq!(cache.len(), 0);
280 assert!(cache.get("SELECT 1 FROM DUAL").is_none());
281 }
282
283 #[test]
284 fn test_ddl_not_cached() {
285 let mut cache = StatementCache::new(5);
286
287 let mut stmt = Statement::new("CREATE TABLE test (id NUMBER)");
288 stmt.set_cursor_id(100);
289 cache.put("CREATE TABLE test (id NUMBER)".to_string(), stmt);
290
291 assert_eq!(cache.len(), 0);
292 }
293
294 #[test]
295 fn test_no_cursor_not_cached() {
296 let mut cache = StatementCache::new(5);
297
298 let stmt = Statement::new("SELECT 1 FROM DUAL");
300 cache.put("SELECT 1 FROM DUAL".to_string(), stmt);
301
302 assert_eq!(cache.len(), 0);
303 }
304
305 #[test]
306 fn test_lru_eviction() {
307 let mut cache = StatementCache::new(3);
308
309 cache.put(
311 "SELECT 1 FROM DUAL".to_string(),
312 make_test_statement("SELECT 1 FROM DUAL", 1),
313 );
314 cache.put(
315 "SELECT 2 FROM DUAL".to_string(),
316 make_test_statement("SELECT 2 FROM DUAL", 2),
317 );
318 cache.put(
319 "SELECT 3 FROM DUAL".to_string(),
320 make_test_statement("SELECT 3 FROM DUAL", 3),
321 );
322
323 assert_eq!(cache.len(), 3);
324
325 cache.get("SELECT 1 FROM DUAL");
327 cache.return_statement("SELECT 1 FROM DUAL");
328
329 cache.put(
331 "SELECT 4 FROM DUAL".to_string(),
332 make_test_statement("SELECT 4 FROM DUAL", 4),
333 );
334
335 assert_eq!(cache.len(), 3);
336 assert!(cache.get("SELECT 2 FROM DUAL").is_none()); assert!(cache.get("SELECT 1 FROM DUAL").is_some()); let to_close = cache.take_cursors_to_close();
341 assert!(to_close.contains(&2));
342 }
343
344 #[test]
345 fn test_in_use_not_returned() {
346 let mut cache = StatementCache::new(5);
347
348 cache.put(
349 "SELECT 1 FROM DUAL".to_string(),
350 make_test_statement("SELECT 1 FROM DUAL", 100),
351 );
352
353 let _ = cache.get("SELECT 1 FROM DUAL");
355
356 assert!(cache.get("SELECT 1 FROM DUAL").is_none());
358
359 cache.return_statement("SELECT 1 FROM DUAL");
361
362 assert!(cache.get("SELECT 1 FROM DUAL").is_some());
364 }
365
366 #[test]
367 fn test_clear() {
368 let mut cache = StatementCache::new(5);
369
370 cache.put(
371 "SELECT 1 FROM DUAL".to_string(),
372 make_test_statement("SELECT 1 FROM DUAL", 1),
373 );
374 cache.put(
375 "SELECT 2 FROM DUAL".to_string(),
376 make_test_statement("SELECT 2 FROM DUAL", 2),
377 );
378
379 assert_eq!(cache.len(), 2);
380
381 cache.clear();
382
383 assert_eq!(cache.len(), 0);
384
385 let to_close = cache.take_cursors_to_close();
387 assert_eq!(to_close.len(), 2);
388 assert!(to_close.contains(&1));
389 assert!(to_close.contains(&2));
390 }
391
392 #[test]
393 fn test_update_existing() {
394 let mut cache = StatementCache::new(5);
395
396 cache.put(
397 "SELECT 1 FROM DUAL".to_string(),
398 make_test_statement("SELECT 1 FROM DUAL", 100),
399 );
400
401 cache.put(
403 "SELECT 1 FROM DUAL".to_string(),
404 make_test_statement("SELECT 1 FROM DUAL", 200),
405 );
406
407 assert_eq!(cache.len(), 1);
408
409 let cached = cache.get("SELECT 1 FROM DUAL").unwrap();
410 assert_eq!(cached.cursor_id(), 200);
411 }
412}