1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
12pub enum SortOrder {
13 Asc,
14 Desc,
15}
16
17#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
19pub enum FilterOp {
20 Eq,
21 Neq,
22 Gt,
23 Gte,
24 Lt,
25 Lte,
26 Contains,
27 StartsWith,
28 EndsWith,
29 In,
30 NotIn,
31 Regex,
32 Exists,
33 TypeIs,
34}
35
36impl FilterOp {
37 pub fn as_str(&self) -> &'static str {
39 match self {
40 FilterOp::Eq => "eq",
41 FilterOp::Neq => "neq",
42 FilterOp::Gt => "gt",
43 FilterOp::Gte => "gte",
44 FilterOp::Lt => "lt",
45 FilterOp::Lte => "lte",
46 FilterOp::Contains => "contains",
47 FilterOp::StartsWith => "starts_with",
48 FilterOp::EndsWith => "ends_with",
49 FilterOp::In => "in",
50 FilterOp::NotIn => "not_in",
51 FilterOp::Regex => "regex",
52 FilterOp::Exists => "exists",
53 FilterOp::TypeIs => "type_is",
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Filter {
61 pub field: String,
62 pub op: String,
63 pub value: Option<serde_json::Value>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, Default)]
68pub struct QueryPlan {
69 pub filters: Vec<Filter>,
70 pub search_terms: Vec<String>,
71 pub sort_field: String,
72 pub sort_order: String,
73 pub page: usize,
74 pub page_size: usize,
75 pub estimated_cost: f64,
76 pub index_used: String,
77 pub optimization_hints: Vec<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct QueryCache {
83 pub key: String,
84 pub result: Vec<HashMap<String, serde_json::Value>>,
85 pub total: usize,
86 pub created_at: f64,
87 pub ttl: f64,
88 pub hit_count: usize,
89 pub query_plan_hash: String,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct QueryResult {
95 pub tiles: Vec<HashMap<String, serde_json::Value>>,
96 pub total: usize,
97 pub page: usize,
98 pub page_size: usize,
99 pub query_time_ms: f64,
100 pub facets: HashMap<String, HashMap<String, usize>>,
101 pub cache_hit: bool,
102 pub plan: Option<QueryPlan>,
103 pub timed_out: bool,
104}
105
106impl Default for QueryResult {
107 fn default() -> Self {
108 Self {
109 tiles: Vec::new(),
110 total: 0,
111 page: 1,
112 page_size: 20,
113 query_time_ms: 0.0,
114 facets: HashMap::new(),
115 cache_hit: false,
116 plan: None,
117 timed_out: false,
118 }
119 }
120}
121
122pub struct TileQueryBuilder {
124 filters: Vec<Filter>,
125 domain: String,
126 search: String,
127 tags: Vec<String>,
128 sort_by: String,
129 sort_order: SortOrder,
130 page: usize,
131 page_size: usize,
132 fields: Vec<String>,
133 include_deleted: bool,
134 explain: bool,
135 cache_ttl: f64,
136 timeout_ms: f64,
137 facet_fields: Vec<String>,
138 post_filter_fn: Option<Box<dyn Fn(&HashMap<String, serde_json::Value>) -> bool + Send + Sync>>,
139}
140
141impl Default for TileQueryBuilder {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147impl TileQueryBuilder {
148 pub fn new() -> Self {
149 Self {
150 filters: Vec::new(),
151 domain: String::new(),
152 search: String::new(),
153 tags: Vec::new(),
154 sort_by: "created_at".to_string(),
155 sort_order: SortOrder::Desc,
156 page: 1,
157 page_size: 20,
158 fields: Vec::new(),
159 include_deleted: false,
160 explain: false,
161 cache_ttl: 0.0,
162 timeout_ms: 5000.0,
163 facet_fields: Vec::new(),
164 post_filter_fn: None,
165 }
166 }
167
168 pub fn filter(mut self, field: &str, op: FilterOp, value: Option<serde_json::Value>) -> Self {
169 self.filters.push(Filter {
170 field: field.to_string(),
171 op: op.as_str().to_string(),
172 value,
173 });
174 self
175 }
176
177 pub fn search(mut self, query: &str) -> Self {
178 self.search = query.to_string();
179 self
180 }
181
182 pub fn in_domain(mut self, domain: &str) -> Self {
183 self.domain = domain.to_string();
184 self
185 }
186
187 pub fn with_tags(mut self, tags: &[&str]) -> Self {
188 self.tags.extend(tags.iter().map(|t| t.to_string()));
189 self
190 }
191
192 pub fn sort_by(mut self, field: &str, order: SortOrder) -> Self {
193 self.sort_by = field.to_string();
194 self.sort_order = order;
195 self
196 }
197
198 pub fn page(mut self, num: usize, size: usize) -> Self {
199 self.page = num.max(1);
200 self.page_size = size.clamp(1, 100);
201 self
202 }
203
204 pub fn select(mut self, fields: &[&str]) -> Self {
205 self.fields = fields.iter().map(|f| f.to_string()).collect();
206 self
207 }
208
209 pub fn include_deleted(mut self) -> Self {
210 self.include_deleted = true;
211 self
212 }
213
214 pub fn explain(mut self) -> Self {
215 self.explain = true;
216 self
217 }
218
219 pub fn cache(mut self, ttl: f64) -> Self {
220 self.cache_ttl = ttl;
221 self
222 }
223
224 pub fn timeout(mut self, ms: f64) -> Self {
225 self.timeout_ms = ms;
226 self
227 }
228
229 pub fn facet(mut self, fields: &[&str]) -> Self {
230 self.facet_fields = fields.iter().map(|f| f.to_string()).collect();
231 self
232 }
233
234 pub fn post_filter<F>(mut self, f: F) -> Self
235 where
236 F: Fn(&HashMap<String, serde_json::Value>) -> bool + Send + Sync + 'static,
237 {
238 self.post_filter_fn = Some(Box::new(f));
239 self
240 }
241
242 pub fn reset(&mut self) -> &mut Self {
243 self.filters.clear();
244 self.domain.clear();
245 self.search.clear();
246 self.tags.clear();
247 self.sort_by = "created_at".to_string();
248 self.sort_order = SortOrder::Desc;
249 self.page = 1;
250 self.page_size = 20;
251 self.fields.clear();
252 self.include_deleted = false;
253 self.explain = false;
254 self.cache_ttl = 0.0;
255 self.facet_fields.clear();
256 self.post_filter_fn = None;
257 self
258 }
259
260 pub fn build_plan(&self) -> QueryPlan {
261 let mut plan = QueryPlan {
262 filters: self.filters.clone(),
263 search_terms: if self.search.is_empty() {
264 Vec::new()
265 } else {
266 self.search
267 .split(|c: char| !c.is_alphanumeric())
268 .filter(|s| !s.is_empty())
269 .map(|s| s.to_string())
270 .collect()
271 },
272 sort_field: self.sort_by.clone(),
273 sort_order: if self.sort_order == SortOrder::Asc {
274 "asc".to_string()
275 } else {
276 "desc".to_string()
277 },
278 page: self.page,
279 page_size: self.page_size,
280 ..Default::default()
281 };
282
283 plan.estimated_cost = self.filters.len() as f64 * 0.1;
284 if !self.search.is_empty() {
285 plan.estimated_cost += 0.3;
286 }
287 if self.post_filter_fn.is_some() {
288 plan.estimated_cost += 0.2;
289 }
290 plan.estimated_cost = plan.estimated_cost.min(1.0);
291
292 if self.filters.is_empty() && self.search.is_empty() {
293 plan.optimization_hints
294 .push("Full table scan — add filters to reduce cost".to_string());
295 }
296 if self.page > 10 {
297 plan.optimization_hints
298 .push("Deep pagination — consider cursor-based approach".to_string());
299 }
300 if self.page_size > 50 {
301 plan.optimization_hints
302 .push("Large page size — may impact latency".to_string());
303 }
304
305 plan
306 }
307
308 pub fn to_dict(&self) -> HashMap<String, serde_json::Value> {
309 let mut map = HashMap::new();
310 map.insert(
311 "filters".to_string(),
312 serde_json::to_value(&self.filters).unwrap_or_default(),
313 );
314 map.insert("domain".to_string(), self.domain.clone().into());
315 map.insert("search".to_string(), self.search.clone().into());
316 map.insert("tags".to_string(), self.tags.clone().into());
317 map.insert("sort_by".to_string(), self.sort_by.clone().into());
318 map.insert(
319 "sort_order".to_string(),
320 if self.sort_order == SortOrder::Asc {
321 "asc"
322 } else {
323 "desc"
324 }
325 .into(),
326 );
327 map.insert("page".to_string(), self.page.into());
328 map.insert("page_size".to_string(), self.page_size.into());
329 map.insert("fields".to_string(), self.fields.clone().into());
330 map.insert("include_deleted".to_string(), self.include_deleted.into());
331 map.insert("facet_fields".to_string(), self.facet_fields.clone().into());
332 map
333 }
334
335 pub fn filter_count(&self) -> usize {
336 self.filters.len()
337 }
338
339 pub fn has_search(&self) -> bool {
340 !self.search.trim().is_empty()
341 }
342
343 pub fn complexity(&self) -> &'static str {
344 let n = self.filters.len()
345 + if self.search.is_empty() { 0 } else { 1 }
346 + self.tags.len();
347 if n == 0 {
348 "simple"
349 } else if n <= 3 {
350 "moderate"
351 } else {
352 "complex"
353 }
354 }
355}
356
357pub struct QueryCacheManager {
359 cache: HashMap<String, QueryCache>,
360 max_size: usize,
361 default_ttl: f64,
362 hits: usize,
363 misses: usize,
364}
365
366impl Default for QueryCacheManager {
367 fn default() -> Self {
368 Self::new(100, 60.0)
369 }
370}
371
372impl QueryCacheManager {
373 pub fn new(max_size: usize, default_ttl: f64) -> Self {
374 Self {
375 cache: HashMap::new(),
376 max_size,
377 default_ttl,
378 hits: 0,
379 misses: 0,
380 }
381 }
382
383 fn make_key(&self, query_dict: &HashMap<String, serde_json::Value>) -> String {
384 let raw = serde_json::to_string(query_dict).unwrap_or_default();
385 md5::compute(raw.as_bytes())
386 .iter()
387 .map(|b| format!("{:02x}", b))
388 .collect::<String>()[..16]
389 .to_string()
390 }
391
392 pub fn get(&mut self, query_dict: &HashMap<String, serde_json::Value>) -> Option<&QueryCache> {
393 let key = self.make_key(query_dict);
394 let now = current_time();
395 if let Some(entry) = self.cache.get(&key) {
396 if now - entry.created_at < entry.ttl {
397 if let Some(e) = self.cache.get_mut(&key) {
400 e.hit_count += 1;
401 }
402 self.hits += 1;
403 return self.cache.get(&key);
404 }
405 }
406 self.misses += 1;
407 None
408 }
409
410 pub fn put(
411 &mut self,
412 query_dict: &HashMap<String, serde_json::Value>,
413 tiles: Vec<HashMap<String, serde_json::Value>>,
414 total: usize,
415 ttl: f64,
416 ) {
417 if self.cache.len() >= self.max_size {
418 self.evict();
419 }
420 let key = self.make_key(query_dict);
421 self.cache.insert(
422 key.clone(),
423 QueryCache {
424 key,
425 result: tiles,
426 total,
427 created_at: current_time(),
428 ttl: if ttl > 0.0 { ttl } else { self.default_ttl },
429 hit_count: 0,
430 query_plan_hash: self.make_key(query_dict),
431 },
432 );
433 }
434
435 fn evict(&mut self) {
436 if self.cache.is_empty() {
437 return;
438 }
439 let oldest = self
440 .cache
441 .iter()
442 .min_by(|a, b| a.1.created_at.partial_cmp(&b.1.created_at).unwrap())
443 .map(|(k, _)| k.clone());
444 if let Some(k) = oldest {
445 self.cache.remove(&k);
446 }
447 }
448
449 pub fn invalidate(&mut self, query_dict: Option<&HashMap<String, serde_json::Value>>) {
450 if let Some(qd) = query_dict {
451 let key = self.make_key(qd);
452 self.cache.remove(&key);
453 } else {
454 self.cache.clear();
455 }
456 }
457
458 pub fn clear(&mut self) {
459 self.cache.clear();
460 }
461
462 pub fn stats(&self) -> HashMap<String, serde_json::Value> {
463 let total = self.hits + self.misses;
464 let mut map = HashMap::new();
465 map.insert("size".to_string(), self.cache.len().into());
466 map.insert("max_size".to_string(), self.max_size.into());
467 map.insert("hits".to_string(), self.hits.into());
468 map.insert("misses".to_string(), self.misses.into());
469 map.insert(
470 "hit_rate".to_string(),
471 if total > 0 {
472 (self.hits as f64) / (total as f64)
473 } else {
474 0.0
475 }
476 .into(),
477 );
478 map
479 }
480}
481
482fn current_time() -> f64 {
483 use std::time::{SystemTime, UNIX_EPOCH};
484 SystemTime::now()
485 .duration_since(UNIX_EPOCH)
486 .map(|d| d.as_secs_f64())
487 .unwrap_or(0.0)
488}
489
490mod md5 {
492 pub fn compute(input: &[u8]) -> [u8; 16] {
493 let mut hash: u64 = 0xcbf29ce484222325;
495 for &byte in input {
496 hash ^= byte as u64;
497 hash = hash.wrapping_mul(0x100000001b3);
498 }
499 let mut out = [0u8; 16];
500 out[..8].copy_from_slice(&hash.to_le_bytes());
501 out[8..].copy_from_slice(&hash.to_le_bytes());
502 out
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_builder_chain() {
512 let builder = TileQueryBuilder::new()
513 .filter("status", FilterOp::Eq, Some("active".into()))
514 .search("hello world")
515 .in_domain("main")
516 .with_tags(&["a", "b"])
517 .sort_by("name", SortOrder::Asc)
518 .page(2, 50)
519 .select(&["id", "name"]);
520
521 assert_eq!(builder.filter_count(), 1);
522 assert!(builder.has_search());
523 assert_eq!(builder.complexity(), "complex");
524 }
525
526 #[test]
527 fn test_build_plan() {
528 let builder = TileQueryBuilder::new()
529 .filter("x", FilterOp::Gt, Some(10.into()))
530 .search("term");
531 let plan = builder.build_plan();
532 assert_eq!(plan.filters.len(), 1);
533 assert_eq!(plan.search_terms, vec!["term"]);
534 assert!(plan.estimated_cost > 0.0);
535 }
536
537 #[test]
538 fn test_cache_manager() {
539 let mut cm = QueryCacheManager::new(10, 60.0);
540 let mut qd = HashMap::new();
541 qd.insert("x".to_string(), serde_json::Value::from(1));
542 cm.put(&qd, vec![HashMap::new()], 1, 0.0);
543 assert!(cm.get(&qd).is_some());
544 assert_eq!(cm.stats()["hits"], serde_json::Value::from(1));
545 }
546}