|
19 | 19 | #include <cstring> |
20 | 20 | #include <string> |
21 | 21 | #include <unordered_map> |
| 22 | +#include <list> |
22 | 23 | #include <memory> |
23 | 24 |
|
24 | 25 | namespace sql_engine { |
@@ -72,17 +73,25 @@ class Session { |
72 | 73 | pool_ = pool; |
73 | 74 | } |
74 | 75 |
|
| 76 | + // Configure the maximum number of cached plans. When the cache is full, |
| 77 | + // the least-recently-used entry is evicted on insert. Default is 1024. |
| 78 | + // Setting to 0 disables caching entirely. |
| 79 | + void set_plan_cache_max_size(size_t n) { plan_cache_max_size_ = n; } |
| 80 | + size_t plan_cache_size() const { return plan_cache_.size(); } |
| 81 | + |
75 | 82 | // Execute a SELECT query. Returns a ResultSet. |
76 | 83 | // Uses plan caching: repeated identical SQL strings skip parse/plan/optimize/distribute. |
77 | 84 | ResultSet execute_query(const char* sql, size_t len) { |
78 | 85 | // Check plan cache first |
79 | 86 | std::string sql_key(sql, len); |
80 | 87 | auto cache_it = plan_cache_.find(sql_key); |
81 | 88 | if (cache_it != plan_cache_.end()) { |
82 | | - // Cache hit: reuse the cached plan. Use exec_arena_ for per-query |
83 | | - // allocations (rows, operator internals). Reset it each time. |
| 89 | + // Cache hit: move this entry to the front of the LRU list. |
| 90 | + plan_cache_order_.splice(plan_cache_order_.begin(), |
| 91 | + plan_cache_order_, |
| 92 | + cache_it->second); |
84 | 93 | exec_arena_.reset(); |
85 | | - auto& entry = cache_it->second; |
| 94 | + auto& entry = *cache_it->second; |
86 | 95 | PlanExecutor<D> executor(functions_, catalog_, exec_arena_); |
87 | 96 | wire_executor(executor); |
88 | 97 | return executor.execute(entry.plan); |
@@ -115,11 +124,10 @@ class Session { |
115 | 124 | wire_executor(executor); |
116 | 125 | ResultSet rs = executor.execute(plan); |
117 | 126 |
|
118 | | - // Cache the plan (parser arena keeps plan tree and strings alive) |
119 | | - CachedPlan entry; |
120 | | - entry.parser = std::move(cached_parser); |
121 | | - entry.plan = plan; |
122 | | - plan_cache_.emplace(std::move(sql_key), std::move(entry)); |
| 127 | + // Cache the plan, enforcing the LRU bound. The parser arena is kept |
| 128 | + // alive via unique_ptr stored in the CachedPlan so all plan/AST |
| 129 | + // string pointers remain valid for subsequent cache hits. |
| 130 | + insert_into_plan_cache(std::move(sql_key), std::move(cached_parser), plan); |
123 | 131 |
|
124 | 132 | return rs; |
125 | 133 | } |
@@ -246,13 +254,45 @@ class Session { |
246 | 254 | // Externally owned; set via set_thread_pool(). Shared across sessions. |
247 | 255 | ThreadPool* pool_ = nullptr; |
248 | 256 |
|
249 | | - // Plan cache: maps SQL string → cached plan + parser arena. |
250 | | - // The parser's arena keeps all AST/plan string pointers valid. |
| 257 | + // Plan cache: bounded LRU keyed by SQL string. Each entry owns the |
| 258 | + // parser whose arena keeps the plan tree and all AST/plan string |
| 259 | + // pointers alive for as long as the entry stays in the cache. |
| 260 | + // |
| 261 | + // Implementation: a list keeps insertion/use order (front = most |
| 262 | + // recently used) and a hash map maps each SQL string to its iterator |
| 263 | + // in the list, giving O(1) lookup, O(1) move-to-front on hit, and |
| 264 | + // O(1) eviction of the LRU entry on insert. |
251 | 265 | struct CachedPlan { |
| 266 | + std::string key; |
252 | 267 | std::unique_ptr<sql_parser::Parser<D>> parser; |
253 | 268 | PlanNode* plan; |
254 | 269 | }; |
255 | | - std::unordered_map<std::string, CachedPlan> plan_cache_; |
| 270 | + using CacheList = std::list<CachedPlan>; |
| 271 | + using CacheIter = typename CacheList::iterator; |
| 272 | + CacheList plan_cache_order_; |
| 273 | + std::unordered_map<std::string, CacheIter> plan_cache_; |
| 274 | + size_t plan_cache_max_size_ = 1024; |
| 275 | + |
| 276 | + void insert_into_plan_cache(std::string key, |
| 277 | + std::unique_ptr<sql_parser::Parser<D>> parser, |
| 278 | + PlanNode* plan) { |
| 279 | + if (plan_cache_max_size_ == 0) return; // caching disabled |
| 280 | + // Evict LRU entries until we're within budget. We evict before insert |
| 281 | + // so the new entry counts against the cap and we never exceed it. |
| 282 | + while (plan_cache_.size() >= plan_cache_max_size_ && !plan_cache_order_.empty()) { |
| 283 | + const std::string& victim_key = plan_cache_order_.back().key; |
| 284 | + plan_cache_.erase(victim_key); |
| 285 | + plan_cache_order_.pop_back(); |
| 286 | + } |
| 287 | + CachedPlan entry; |
| 288 | + entry.key = std::move(key); |
| 289 | + entry.parser = std::move(parser); |
| 290 | + entry.plan = plan; |
| 291 | + plan_cache_order_.push_front(std::move(entry)); |
| 292 | + // The map stores the iterator and a copy of the key (so the lookup |
| 293 | + // string and the entry's owned key both remain valid through moves). |
| 294 | + plan_cache_[plan_cache_order_.front().key] = plan_cache_order_.begin(); |
| 295 | + } |
256 | 296 |
|
257 | 297 | void wire_executor(PlanExecutor<D>& executor) { |
258 | 298 | for (auto& kv : sources_) |
|
0 commit comments