1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Arc, RwLock};
3
4use sift_query_algorithm::BoundedLruMap;
5use sift_query_core::ConfigError;
6
7use crate::{QueryShapeId, RenderedPgQuery};
8
9mod template;
10use crate::{PgRenderer, RenderError};
11use sift_query_core::{FieldId, QueryPlan, SearchEntity, SearchOptions};
12pub(crate) use template::RenderRequest;
13use template::{Template, TemplateKey};
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16enum CacheKey {
17 Rendered(QueryShapeId),
18 Template(Arc<TemplateKey>),
19}
20
21#[derive(Debug, Clone)]
22enum CacheEntry {
23 Sql(Arc<str>),
24 Template(Arc<Template>),
25}
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct ShapeCacheStats {
30 pub hits: u64,
31 pub misses: u64,
32 pub insertions: u64,
33 pub evictions: u64,
34 pub collisions: u64,
35 pub entries: usize,
36 pub capacity: usize,
37}
38
39#[derive(Debug)]
45pub struct QueryShapeCache {
46 state: RwLock<CacheState>,
47}
48
49#[derive(Debug)]
50struct CacheState {
51 entries: BoundedLruMap<CacheKey, CacheEntry>,
52 hits: AtomicU64,
53 misses: u64,
54 insertions: u64,
55 evictions: u64,
56 collisions: u64,
57}
58
59impl QueryShapeCache {
60 pub fn new(capacity: usize) -> Result<Self, ConfigError> {
61 let entries = BoundedLruMap::new(capacity)
62 .map_err(|error| ConfigError::Invalid(format!("shape cache {error}")))?;
63 Ok(Self {
64 state: RwLock::new(CacheState {
65 entries,
66 hits: AtomicU64::new(0),
67 misses: 0,
68 insertions: 0,
69 evictions: 0,
70 collisions: 0,
71 }),
72 })
73 }
74
75 #[must_use]
77 pub fn intern(&self, mut query: RenderedPgQuery) -> (RenderedPgQuery, bool) {
78 let mut state = self
79 .state
80 .write()
81 .unwrap_or_else(std::sync::PoisonError::into_inner);
82 let key = CacheKey::Rendered(query.shape_id);
83 if let Some(CacheEntry::Sql(sql)) = state.entries.get(&key).cloned() {
84 if sql.as_ref() == query.sql.as_ref() {
85 query.sql = sql;
86 increment_hits(&state.hits);
87 return (query, true);
88 }
89 state.collisions = state.collisions.saturating_add(1);
90 state.misses = state.misses.saturating_add(1);
91 return (query, false);
92 }
93
94 state.misses = state.misses.saturating_add(1);
95 if state
96 .entries
97 .insert(key, CacheEntry::Sql(Arc::clone(&query.sql)))
98 {
99 state.evictions = state.evictions.saturating_add(1);
100 }
101 state.insertions = state.insertions.saturating_add(1);
102 (query, false)
103 }
104
105 pub fn render_list<E: SearchEntity>(
107 &self,
108 plan: &QueryPlan<E>,
109 ) -> Result<(RenderedPgQuery, bool), RenderError> {
110 self.render(plan, &PgRenderer, &RenderRequest::List)
111 }
112
113 pub fn render_cluster<E: SearchEntity>(
114 &self,
115 plan: &QueryPlan<E>,
116 options: Option<&SearchOptions<E>>,
117 ) -> Result<(RenderedPgQuery, bool), RenderError> {
118 self.render(plan, &PgRenderer, &RenderRequest::Cluster(options))
119 }
120
121 pub fn render_sum<E: SearchEntity>(
122 &self,
123 plan: &QueryPlan<E>,
124 field: FieldId,
125 ) -> Result<(RenderedPgQuery, bool), RenderError> {
126 self.render(plan, &PgRenderer, &RenderRequest::Sum(field))
127 }
128
129 pub fn render_window_list<E: SearchEntity>(
130 &self,
131 plan: &QueryPlan<E>,
132 ) -> Result<(RenderedPgQuery, bool), RenderError> {
133 self.render(plan, &PgRenderer, &RenderRequest::Window)
134 }
135
136 pub(crate) fn render<E: SearchEntity>(
137 &self,
138 plan: &QueryPlan<E>,
139 renderer: &PgRenderer,
140 request: &RenderRequest<'_, E>,
141 ) -> Result<(RenderedPgQuery, bool), RenderError> {
142 if matches!(request, RenderRequest::Window) && !renderer.supports_window_count(plan) {
143 return Err(RenderError::WrongTarget);
144 }
145 let key = CacheKey::Template(Arc::new(TemplateKey::new(plan, request)?));
146 {
149 let state = self
150 .state
151 .read()
152 .unwrap_or_else(std::sync::PoisonError::into_inner);
153 if let Some(CacheEntry::Template(template)) = state.entries.peek_mru(&key) {
154 let template = Arc::clone(template);
155 increment_hits(&state.hits);
156 drop(state);
157 return template.bind(plan).map(|query| (query, true));
158 }
159 }
160 {
161 let mut state = self
162 .state
163 .write()
164 .unwrap_or_else(std::sync::PoisonError::into_inner);
165 if let Some(CacheEntry::Template(template)) = state.entries.get(&key).cloned() {
166 increment_hits(&state.hits);
167 drop(state);
168 return template.bind(plan).map(|query| (query, true));
169 }
170 state.misses = state.misses.saturating_add(1);
171 }
172 let query = request.render(renderer, plan)?;
174 let template = Arc::new(Template::new(plan, request, &query)?);
175 let mut state = self
176 .state
177 .write()
178 .unwrap_or_else(std::sync::PoisonError::into_inner);
179 if state.entries.insert(key, CacheEntry::Template(template)) {
180 state.evictions = state.evictions.saturating_add(1);
181 }
182 state.insertions = state.insertions.saturating_add(1);
183 Ok((query, false))
184 }
185
186 #[must_use]
187 pub fn stats(&self) -> ShapeCacheStats {
188 let state = self
189 .state
190 .read()
191 .unwrap_or_else(std::sync::PoisonError::into_inner);
192 ShapeCacheStats {
193 hits: state.hits.load(Ordering::Relaxed),
194 misses: state.misses,
195 insertions: state.insertions,
196 evictions: state.evictions,
197 collisions: state.collisions,
198 entries: state.entries.len(),
199 capacity: state.entries.capacity(),
200 }
201 }
202
203 pub fn clear(&self) {
204 let mut state = self
205 .state
206 .write()
207 .unwrap_or_else(std::sync::PoisonError::into_inner);
208 state.entries.clear();
209 }
210}
211
212fn increment_hits(hits: &AtomicU64) {
213 let _ = hits.try_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
214 Some(count.saturating_add(1))
215 });
216}