1use dashmap::DashMap;
6use std::sync::Arc;
7
8use linkrs_core::types::Index;
9
10use super::{EdgeTypeStatistics, PropertyCombinationStats, PropertyStatistics, TagStatistics};
11
12#[derive(Debug)]
17pub struct StatisticsManager {
18 tag_stats: Arc<DashMap<String, TagStatistics>>,
20 tag_id_to_name: Arc<DashMap<String, String>>,
22 edge_stats: Arc<DashMap<String, EdgeTypeStatistics>>,
24 property_stats: Arc<DashMap<String, PropertyStatistics>>,
26 property_combo_stats: Arc<DashMap<String, PropertyCombinationStats>>,
29 space_versions: Arc<DashMap<String, (u64, u64)>>,
34 index_catalog: Arc<DashMap<String, (i32, Vec<Index>)>>,
38}
39
40impl StatisticsManager {
41 pub fn new() -> Self {
43 Self {
44 tag_stats: Arc::new(DashMap::new()),
45 tag_id_to_name: Arc::new(DashMap::new()),
46 edge_stats: Arc::new(DashMap::new()),
47 property_stats: Arc::new(DashMap::new()),
48 property_combo_stats: Arc::new(DashMap::new()),
49 space_versions: Arc::new(DashMap::new()),
50 index_catalog: Arc::new(DashMap::new()),
51 }
52 }
53
54 fn composite_key(space: &str, name: &str) -> String {
56 format!("{}.{}", space, name)
57 }
58
59 fn split_composite_key<'a>(key: &'a str, name: &str) -> Option<&'a str> {
61 key.strip_suffix(&format!(".{}", name))
62 }
63
64 pub fn mark_space_dirty(&self, space: &str) {
66 self.space_versions.remove(space);
67 }
68
69 pub fn invalidate_space(&self, space: Option<&str>) {
71 match space {
72 Some(space) => self.mark_space_dirty(space),
73 None => self.space_versions.clear(),
74 }
75 }
76
77 pub fn space_stamp(&self, space: &str) -> Option<(u64, u64)> {
80 self.space_versions.get(space).map(|v| *v)
81 }
82
83 pub fn space_version(&self, space: &str) -> Option<u64> {
85 self.space_stamp(space).map(|stamp| stamp.0)
86 }
87
88 pub fn set_space_stamp(&self, space: &str, schema_version: u64, data_epoch: u64) {
93 if schema_version == 0 || data_epoch == 0 {
94 self.space_versions.remove(space);
95 return;
96 }
97 self.space_versions
98 .insert(space.to_string(), (schema_version, data_epoch));
99 }
100
101 pub fn register_tag_indexes(
107 &self,
108 space: &str,
109 tag_name: &str,
110 tag_id: i32,
111 indexes: Vec<Index>,
112 ) {
113 self.index_catalog
114 .insert(Self::composite_key(space, tag_name), (tag_id, indexes));
115 }
116
117 pub fn get_tag_indexes(&self, space: &str, tag_name: &str) -> Option<(i32, Vec<Index>)> {
119 self.index_catalog
120 .get(&Self::composite_key(space, tag_name))
121 .map(|entry| entry.clone())
122 }
123
124 pub fn register_tag_id(&self, space: &str, tag_id: i32, tag_name: String) {
129 self.tag_id_to_name
130 .insert(format!("{space}:{tag_id}"), tag_name);
131 }
132
133 pub fn get_tag_name_by_id(&self, space: &str, tag_id: i32) -> Option<String> {
135 self.tag_id_to_name
136 .get(&format!("{space}:{tag_id}"))
137 .map(|v| v.clone())
138 }
139
140 pub fn get_tag_stats_by_id(&self, space: &str, tag_id: i32) -> Option<TagStatistics> {
142 let tag_name = self.get_tag_name_by_id(space, tag_id)?;
143 self.get_tag_stats(space, &tag_name)
144 }
145
146 pub fn get_vertex_count_by_id(&self, space: &str, tag_id: i32) -> u64 {
148 self.get_tag_stats_by_id(space, tag_id)
149 .map(|s| s.vertex_count)
150 .unwrap_or(0)
151 }
152
153 pub fn get_tag_stats(&self, space: &str, tag_name: &str) -> Option<TagStatistics> {
155 self.tag_stats
156 .get(&Self::composite_key(space, tag_name))
157 .map(|v| v.clone())
158 }
159
160 pub fn update_tag_stats(&self, space: &str, stats: TagStatistics) {
162 self.tag_stats
163 .insert(Self::composite_key(space, &stats.tag_name), stats);
164 }
165
166 pub fn get_vertex_count(&self, space: &str, tag_name: &str) -> u64 {
168 self.get_tag_stats(space, tag_name)
169 .map(|s| s.vertex_count)
170 .unwrap_or(0)
171 }
172
173 pub fn get_edge_stats(&self, space: &str, edge_type: &str) -> Option<EdgeTypeStatistics> {
175 self.edge_stats
176 .get(&Self::composite_key(space, edge_type))
177 .map(|v| v.clone())
178 }
179
180 pub fn update_edge_stats(&self, space: &str, stats: EdgeTypeStatistics) {
182 self.edge_stats
183 .insert(Self::composite_key(space, &stats.edge_type), stats);
184 }
185
186 pub fn get_edge_count(&self, space: &str, edge_type: &str) -> u64 {
188 self.get_edge_stats(space, edge_type)
189 .map(|s| s.edge_count)
190 .unwrap_or(0)
191 }
192
193 pub fn get_property_stats(
195 &self,
196 space: &str,
197 tag_name: Option<&str>,
198 property_name: &str,
199 ) -> Option<PropertyStatistics> {
200 let key = match tag_name {
201 Some(tag) => format!("{}.{}.{}", space, tag, property_name),
202 None => format!("{}.{}", space, property_name),
203 };
204 self.property_stats.get(&key).map(|v| v.clone())
205 }
206
207 pub fn update_property_stats(&self, space: &str, stats: PropertyStatistics) {
209 let key = match &stats.tag_name {
210 Some(tag) => format!("{}.{}.{}", space, tag, stats.property_name),
211 None => format!("{}.{}", space, stats.property_name),
212 };
213 self.property_stats.insert(key, stats);
214 }
215
216 pub fn clear_all(&self) {
218 self.tag_stats.clear();
219 self.tag_id_to_name.clear();
220 self.edge_stats.clear();
221 self.property_stats.clear();
222 self.property_combo_stats.clear();
223 self.space_versions.clear();
224 self.index_catalog.clear();
225 }
226
227 pub fn get_property_combo_stats(
229 &self,
230 space: &str,
231 tag_name: &str,
232 properties: &[String],
233 ) -> Option<PropertyCombinationStats> {
234 let key = format!("{}.{}.{}", space, tag_name, properties.join("."));
235 self.property_combo_stats.get(&key).map(|v| v.clone())
236 }
237
238 pub fn update_property_combo_stats(&self, space: &str, stats: PropertyCombinationStats) {
242 let tag = stats.tag_name.as_deref().unwrap_or("");
243 let key = format!("{}.{}.{}", space, tag, stats.properties.join("."));
244 if let Some(mut existing) = self.property_combo_stats.get_mut(&key) {
245 existing.update(stats.combined_distinct_values, stats.sample_count);
246 } else {
247 self.property_combo_stats.insert(key, stats);
248 }
249 }
250
251 pub fn get_combined_cardinality(
254 &self,
255 space: &str,
256 tag_name: Option<&str>,
257 properties: &[String],
258 ) -> Option<u64> {
259 let tag = tag_name?;
260 self.get_property_combo_stats(space, tag, properties)
261 .map(|s| s.estimated_cardinality())
262 }
263
264 pub fn get_all_tags(&self) -> Vec<String> {
268 let mut names: Vec<String> = Vec::new();
269 for entry in self.tag_stats.iter() {
270 let name = Self::split_composite_key(entry.key(), &entry.tag_name)
271 .map(|_| entry.tag_name.clone())
272 .unwrap_or_else(|| entry.key().clone());
273 if !names.contains(&name) {
274 names.push(name);
275 }
276 }
277 names
278 }
279
280 pub fn get_all_edge_types(&self) -> Vec<String> {
282 let mut names: Vec<String> = Vec::new();
283 for entry in self.edge_stats.iter() {
284 let name = Self::split_composite_key(entry.key(), &entry.edge_type)
285 .map(|_| entry.edge_type.clone())
286 .unwrap_or_else(|| entry.key().clone());
287 if !names.contains(&name) {
288 names.push(name);
289 }
290 }
291 names
292 }
293}
294
295impl Default for StatisticsManager {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl Clone for StatisticsManager {
302 fn clone(&self) -> Self {
303 Self {
304 tag_stats: Arc::clone(&self.tag_stats),
305 tag_id_to_name: Arc::clone(&self.tag_id_to_name),
306 edge_stats: Arc::clone(&self.edge_stats),
307 property_stats: Arc::clone(&self.property_stats),
308 property_combo_stats: Arc::clone(&self.property_combo_stats),
309 space_versions: Arc::clone(&self.space_versions),
310 index_catalog: Arc::clone(&self.index_catalog),
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::optimizer::stats::edge::EdgeTypeStatistics;
319 use crate::optimizer::stats::property::PropertyStatistics;
320
321 const TEST_SPACE: &str = "test";
322
323 #[test]
324 fn test_statistics_manager_creation() {
325 let manager = StatisticsManager::new();
326 assert_eq!(manager.get_all_tags().len(), 0);
327 assert_eq!(manager.get_all_edge_types().len(), 0);
328 }
329
330 #[test]
331 fn test_statistics_manager_default() {
332 let manager = StatisticsManager::default();
333 assert_eq!(manager.get_all_tags().len(), 0);
334 }
335
336 #[test]
337 fn test_register_and_get_tag_id() {
338 let manager = StatisticsManager::new();
339 manager.register_tag_id(TEST_SPACE, 1, "person".to_string());
340
341 assert_eq!(
342 manager.get_tag_name_by_id(TEST_SPACE, 1),
343 Some("person".to_string())
344 );
345 assert_eq!(manager.get_tag_name_by_id(TEST_SPACE, 2), None);
346 }
347
348 #[test]
349 fn test_add_tag_statistics() {
350 let manager = StatisticsManager::new();
351 let mut stats = TagStatistics::new("person".to_string());
352 stats.vertex_count = 1000;
353 stats.avg_out_degree = 5.5;
354 stats.avg_in_degree = 3.2;
355
356 manager.update_tag_stats(TEST_SPACE, stats.clone());
357
358 let retrieved = manager
359 .get_tag_stats(TEST_SPACE, "person")
360 .expect("Tag stats not found");
361 assert_eq!(retrieved.vertex_count, 1000);
362 assert_eq!(retrieved.avg_out_degree, 5.5);
363 assert_eq!(retrieved.avg_in_degree, 3.2);
364 }
365
366 #[test]
367 fn test_get_vertex_count() {
368 let manager = StatisticsManager::new();
369 let mut stats = TagStatistics::new("company".to_string());
370 stats.vertex_count = 500;
371 manager.update_tag_stats(TEST_SPACE, stats);
372
373 assert_eq!(manager.get_vertex_count(TEST_SPACE, "company"), 500);
374 assert_eq!(manager.get_vertex_count(TEST_SPACE, "nonexistent"), 0);
375 }
376
377 #[test]
378 fn test_get_tag_stats_by_id() {
379 let manager = StatisticsManager::new();
380 manager.register_tag_id(TEST_SPACE, 10, "product".to_string());
381
382 let mut stats = TagStatistics::new("product".to_string());
383 stats.vertex_count = 2000;
384 manager.update_tag_stats(TEST_SPACE, stats);
385
386 let retrieved = manager
387 .get_tag_stats_by_id(TEST_SPACE, 10)
388 .expect("Tag stats not found");
389 assert_eq!(retrieved.vertex_count, 2000);
390 }
391
392 #[test]
393 fn test_get_vertex_count_by_id() {
394 let manager = StatisticsManager::new();
395 manager.register_tag_id(TEST_SPACE, 5, "category".to_string());
396
397 let mut stats = TagStatistics::new("category".to_string());
398 stats.vertex_count = 100;
399 manager.update_tag_stats(TEST_SPACE, stats);
400
401 assert_eq!(manager.get_vertex_count_by_id(TEST_SPACE, 5), 100);
402 assert_eq!(manager.get_vertex_count_by_id(TEST_SPACE, 999), 0);
403 }
404
405 #[test]
406 fn test_add_edge_statistics() {
407 let manager = StatisticsManager::new();
408 let mut edge_stats = EdgeTypeStatistics::new("follows".to_string());
409 edge_stats.edge_count = 5000;
410
411 manager.update_edge_stats(TEST_SPACE, edge_stats.clone());
412
413 let retrieved = manager
414 .get_edge_stats(TEST_SPACE, "follows")
415 .expect("Edge stats not found");
416 assert_eq!(retrieved.edge_count, 5000);
417 }
418
419 #[test]
420 fn test_get_edge_count() {
421 let manager = StatisticsManager::new();
422 let mut edge_stats = EdgeTypeStatistics::new("works_at".to_string());
423 edge_stats.edge_count = 3000;
424 manager.update_edge_stats(TEST_SPACE, edge_stats);
425
426 assert_eq!(manager.get_edge_count(TEST_SPACE, "works_at"), 3000);
427 assert_eq!(manager.get_edge_count(TEST_SPACE, "nonexistent"), 0);
428 }
429
430 #[test]
431 fn test_add_property_statistics() {
432 let manager = StatisticsManager::new();
433 let mut prop_stats = PropertyStatistics::new("age".into(), Some("person".to_string()));
434 prop_stats.distinct_values = 100;
435
436 manager.update_property_stats(TEST_SPACE, prop_stats);
437
438 let retrieved = manager
439 .get_property_stats(TEST_SPACE, Some("person"), "age")
440 .expect("Property stats not found");
441 assert_eq!(retrieved.distinct_values, 100);
442 }
443
444 #[test]
445 fn test_multiple_tags_statistics() {
446 let manager = StatisticsManager::new();
447
448 let mut person_stats = TagStatistics::new("person".to_string());
449 person_stats.vertex_count = 1000;
450 manager.update_tag_stats(TEST_SPACE, person_stats);
451
452 let mut company_stats = TagStatistics::new("company".to_string());
453 company_stats.vertex_count = 500;
454 manager.update_tag_stats(TEST_SPACE, company_stats);
455
456 assert_eq!(manager.get_vertex_count(TEST_SPACE, "person"), 1000);
457 assert_eq!(manager.get_vertex_count(TEST_SPACE, "company"), 500);
458
459 let all_tags = manager.get_all_tags();
460 assert_eq!(all_tags.len(), 2);
461 assert!(all_tags.contains(&"person".to_string()));
462 assert!(all_tags.contains(&"company".to_string()));
463 }
464
465 #[test]
466 fn test_statistics_update_overwrite() {
467 let manager = StatisticsManager::new();
468
469 let mut stats1 = TagStatistics::new("person".to_string());
470 stats1.vertex_count = 1000;
471 manager.update_tag_stats(TEST_SPACE, stats1);
472
473 let mut stats2 = TagStatistics::new("person".to_string());
474 stats2.vertex_count = 2000;
475 manager.update_tag_stats(TEST_SPACE, stats2);
476
477 assert_eq!(manager.get_vertex_count(TEST_SPACE, "person"), 2000);
478 }
479
480 #[test]
481 fn test_clear_all_statistics() {
482 let manager = StatisticsManager::new();
483
484 let mut person_stats = TagStatistics::new("person".to_string());
485 person_stats.vertex_count = 1000;
486 manager.update_tag_stats(TEST_SPACE, person_stats);
487
488 let mut edge_stats = EdgeTypeStatistics::new("follows".to_string());
489 edge_stats.edge_count = 5000;
490 manager.update_edge_stats(TEST_SPACE, edge_stats);
491
492 assert_eq!(manager.get_all_tags().len(), 1);
493 assert_eq!(manager.get_all_edge_types().len(), 1);
494
495 manager.clear_all();
496
497 assert_eq!(manager.get_all_tags().len(), 0);
498 assert_eq!(manager.get_all_edge_types().len(), 0);
499 assert_eq!(manager.get_vertex_count(TEST_SPACE, "person"), 0);
500 assert_eq!(manager.get_edge_count(TEST_SPACE, "follows"), 0);
501 }
502
503 #[test]
504 fn test_statistics_manager_clone() {
505 let manager = StatisticsManager::new();
506
507 let mut stats = TagStatistics::new("person".to_string());
508 stats.vertex_count = 1000;
509 manager.update_tag_stats(TEST_SPACE, stats);
510
511 let cloned = manager.clone();
512 assert_eq!(cloned.get_vertex_count(TEST_SPACE, "person"), 1000);
513
514 let mut new_stats = TagStatistics::new("company".to_string());
515 new_stats.vertex_count = 500;
516 cloned.update_tag_stats(TEST_SPACE, new_stats);
517
518 assert_eq!(manager.get_vertex_count(TEST_SPACE, "company"), 500);
519 }
520
521 #[test]
522 fn test_property_combination_statistics() {
523 let manager = StatisticsManager::new();
524 let props = vec!["city".to_string(), "age".to_string()];
525 let mut combo_stats =
526 PropertyCombinationStats::new(String::new(), Some("person".to_string()), props.clone());
527 combo_stats.combined_distinct_values = 50;
528
529 manager.update_property_combo_stats(TEST_SPACE, combo_stats);
530
531 let retrieved = manager
532 .get_property_combo_stats(TEST_SPACE, "person", &props)
533 .expect("Combo stats not found");
534 assert_eq!(retrieved.combined_distinct_values, 50);
535 }
536
537 #[test]
538 fn test_get_combined_cardinality() {
539 let manager = StatisticsManager::new();
540 let props = vec!["city".to_string(), "age".to_string()];
541 let mut combo_stats =
542 PropertyCombinationStats::new(String::new(), Some("person".to_string()), props.clone());
543 combo_stats.combined_distinct_values = 75;
544
545 manager.update_property_combo_stats(TEST_SPACE, combo_stats);
546
547 let cardinality = manager
548 .get_combined_cardinality(TEST_SPACE, Some("person"), &props)
549 .expect("Combined cardinality not found");
550 assert_eq!(cardinality, 75);
551 }
552
553 #[test]
554 fn test_property_stats_without_tag() {
555 let manager = StatisticsManager::new();
556 let mut prop_stats = PropertyStatistics::new("global_prop".into(), None);
557 prop_stats.distinct_values = 200;
558
559 manager.update_property_stats(TEST_SPACE, prop_stats);
560
561 let retrieved = manager
562 .get_property_stats(TEST_SPACE, None, "global_prop")
563 .expect("Property stats not found");
564 assert_eq!(retrieved.distinct_values, 200);
565 }
566
567 #[test]
568 fn test_multiple_edge_types() {
569 let manager = StatisticsManager::new();
570
571 let mut follows = EdgeTypeStatistics::new("follows".to_string());
572 follows.edge_count = 5000;
573 manager.update_edge_stats(TEST_SPACE, follows);
574
575 let mut works_at = EdgeTypeStatistics::new("works_at".to_string());
576 works_at.edge_count = 3000;
577 manager.update_edge_stats(TEST_SPACE, works_at);
578
579 assert_eq!(manager.get_edge_count(TEST_SPACE, "follows"), 5000);
580 assert_eq!(manager.get_edge_count(TEST_SPACE, "works_at"), 3000);
581
582 let all_edge_types = manager.get_all_edge_types();
583 assert_eq!(all_edge_types.len(), 2);
584 assert!(all_edge_types.contains(&"follows".to_string()));
585 assert!(all_edge_types.contains(&"works_at".to_string()));
586 }
587
588 #[test]
589 fn test_tag_id_isolated_by_space() {
590 let manager = StatisticsManager::new();
591 manager.register_tag_id("a", 7, "person".to_string());
592 manager.register_tag_id("b", 7, "company".to_string());
593
594 assert_eq!(
595 manager.get_tag_name_by_id("a", 7),
596 Some("person".to_string())
597 );
598 assert_eq!(
599 manager.get_tag_name_by_id("b", 7),
600 Some("company".to_string())
601 );
602 }
603
604 #[test]
605 fn test_space_isolation() {
606 let manager = StatisticsManager::new();
607
608 let mut stats = TagStatistics::new("person".to_string());
609 stats.vertex_count = 42;
610 manager.update_tag_stats("basketball", stats.with_version("basketball".into(), 7));
611
612 let mut edge_stats = EdgeTypeStatistics::new("follow".to_string());
613 edge_stats.edge_count = 99;
614 manager.update_edge_stats(
615 "basketball",
616 edge_stats.with_version("basketball".into(), 7),
617 );
618
619 assert_eq!(manager.get_vertex_count("basketball", "person"), 42);
620 assert_eq!(manager.get_edge_count("basketball", "follow"), 99);
621 let tag_stats = manager.get_tag_stats("basketball", "person").expect("tag");
622 assert_eq!(tag_stats.schema_version, Some(7));
623 assert_eq!(tag_stats.space.as_deref(), Some("basketball"));
624
625 assert_eq!(manager.get_vertex_count("tennis", "person"), 0);
627 assert_eq!(manager.get_edge_count("tennis", "follow"), 0);
628 }
629
630 #[test]
631 fn test_space_version_gate() {
632 let manager = StatisticsManager::new();
633 assert_eq!(manager.space_stamp("basketball"), None);
634
635 manager.set_space_stamp("basketball", 3, 100);
636 assert_eq!(manager.space_stamp("basketball"), Some((3, 100)));
637
638 manager.mark_space_dirty("basketball");
639 assert_eq!(manager.space_stamp("basketball"), None);
640
641 manager.set_space_stamp("a", 1, 10);
642 manager.set_space_stamp("b", 2, 20);
643 manager.invalidate_space(None);
644 assert_eq!(manager.space_stamp("a"), None);
645 assert_eq!(manager.space_stamp("b"), None);
646 }
647}