akar_main/connection/
plan_cache.rs1use akar_binder::bound_statement::BoundStatement;
12use akar_planner::logical_operator::LogicalOperator;
13use std::collections::{HashMap, VecDeque};
14use std::sync::Arc;
15
16pub(crate) struct CachedPlan {
23 pub bound: Arc<BoundStatement>,
24 pub plan: Arc<Vec<LogicalOperator>>,
25 pub catalog_version: u64,
26}
27
28pub(crate) struct PlanCache<T> {
31 map: HashMap<String, T>,
32 order: VecDeque<String>,
33 capacity: usize,
34}
35
36impl<T> PlanCache<T> {
37 pub fn new(capacity: usize) -> Self {
38 Self {
39 map: HashMap::new(),
40 order: VecDeque::new(),
41 capacity: capacity.max(1),
42 }
43 }
44
45 pub fn get(&mut self, key: &str) -> Option<&T> {
46 if self.map.contains_key(key) {
47 if let Some(pos) = self.order.iter().position(|k| k == key) {
48 if pos + 1 != self.order.len() {
49 self.order.remove(pos);
50 self.order.push_back(key.to_string());
51 }
52 }
53 }
54 self.map.get(key)
55 }
56
57 pub fn insert(&mut self, key: String, value: T) {
58 if let Some(pos) = self.order.iter().position(|k| k.as_str() == key.as_str()) {
59 self.order.remove(pos);
60 } else if self.map.len() >= self.capacity {
61 if let Some(oldest) = self.order.pop_front() {
62 self.map.remove(&oldest);
63 }
64 }
65 self.order.push_back(key.clone());
66 self.map.insert(key, value);
67 }
68
69 pub fn clear(&mut self) {
70 self.map.clear();
71 self.order.clear();
72 }
73
74 pub fn len(&self) -> usize {
75 self.map.len()
76 }
77
78 #[allow(dead_code)]
80 pub fn capacity(&self) -> usize {
81 self.capacity
82 }
83}
84
85pub fn normalize_query(query: &str) -> String {
93 let trimmed = query.trim();
94 let mut result = String::with_capacity(trimmed.len());
95 let mut prev_space = true;
96 let mut in_single = false;
97 let mut in_double = false;
98 let mut in_backtick = false;
99
100 for ch in trimmed.chars() {
101 if in_single {
102 result.push(ch);
103 if ch == '\'' {
104 in_single = false;
105 }
106 prev_space = false;
107 continue;
108 }
109 if in_double {
110 result.push(ch);
111 if ch == '"' {
112 in_double = false;
113 }
114 prev_space = false;
115 continue;
116 }
117 if in_backtick {
118 result.push(ch);
119 if ch == '`' {
120 in_backtick = false;
121 }
122 prev_space = false;
123 continue;
124 }
125
126 match ch {
127 '\'' => {
128 in_single = true;
129 result.push(ch);
130 prev_space = false;
131 }
132 '"' => {
133 in_double = true;
134 result.push(ch);
135 prev_space = false;
136 }
137 '`' => {
138 in_backtick = true;
139 result.push(ch);
140 prev_space = false;
141 }
142 '\n' | '\r' => {
143 result.push('\n');
145 prev_space = false;
146 }
147 c if c.is_whitespace() => {
148 if !prev_space {
149 result.push(' ');
150 }
151 prev_space = true;
152 }
153 other => {
154 result.push(other);
155 prev_space = false;
156 }
157 }
158 }
159 result
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_normalize_query_trims_and_collapses() {
168 assert_eq!(normalize_query(" MATCH (p) RETURN p "), "MATCH (p) RETURN p");
169 assert_eq!(
170 normalize_query("MATCH\n (p)\tWHERE p.x > 5"),
171 "MATCH\n (p) WHERE p.x > 5"
172 );
173 assert_eq!(normalize_query(""), "");
174 assert_eq!(normalize_query(" "), "");
175 }
176
177 #[test]
178 fn test_normalize_preserves_string_literals() {
179 assert_eq!(normalize_query("RETURN 'a b' AS x"), "RETURN 'a b' AS x");
180 assert_eq!(normalize_query("RETURN \"a b\" AS x"), "RETURN \"a b\" AS x");
181 assert_eq!(
182 normalize_query("MATCH (`a b`) RETURN `c d`"),
183 "MATCH (`a b`) RETURN `c d`"
184 );
185 }
186
187 #[test]
188 fn test_normalize_keeps_newlines_for_comments() {
189 assert_eq!(normalize_query("MATCH (p) // c\nRETURN p"), "MATCH (p) // c\nRETURN p");
190 }
191
192 #[test]
193 fn test_lru_eviction() {
194 let mut cache: PlanCache<u32> = PlanCache::new(2);
195 cache.insert("a".into(), 1);
196 cache.insert("b".into(), 2);
197 cache.insert("c".into(), 3);
198 assert_eq!(cache.len(), 2);
199 assert!(cache.get("a").is_none());
200 assert_eq!(cache.get("b"), Some(&2));
201 assert_eq!(cache.get("c"), Some(&3));
202 }
203
204 #[test]
205 fn test_lru_touch_moves_to_back() {
206 let mut cache: PlanCache<u32> = PlanCache::new(2);
207 cache.insert("a".into(), 1);
208 cache.insert("b".into(), 2);
209 assert_eq!(cache.get("a"), Some(&1));
211 cache.insert("c".into(), 3);
212 assert!(cache.get("b").is_none());
213 assert_eq!(cache.get("a"), Some(&1));
214 assert_eq!(cache.get("c"), Some(&3));
215 }
216
217 #[test]
218 fn test_insert_existing_refreshes() {
219 let mut cache: PlanCache<u32> = PlanCache::new(2);
220 cache.insert("a".into(), 1);
221 cache.insert("b".into(), 2);
222 cache.insert("a".into(), 10);
223 assert_eq!(cache.get("a"), Some(&10));
224 assert_eq!(cache.get("b"), Some(&2));
225 }
226
227 #[test]
228 fn test_clear() {
229 let mut cache: PlanCache<u32> = PlanCache::new(4);
230 cache.insert("a".into(), 1);
231 cache.insert("b".into(), 2);
232 cache.clear();
233 assert_eq!(cache.len(), 0);
234 assert!(cache.get("a").is_none());
235 }
236
237 #[test]
238 fn test_capacity_min_one() {
239 let cache: PlanCache<u32> = PlanCache::new(0);
240 assert_eq!(cache.capacity(), 1);
241 }
242}