1use dashmap::DashMap;
4use parking_lot::RwLock;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use crate::collection::{Collection, CollectionConfig, CollectionStats};
10use crate::error::{CollectionError, Result};
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14struct CollectionMetadata {
15 name: String,
16 config: CollectionConfig,
17 created_at: i64,
18 updated_at: i64,
19}
20
21#[derive(Debug)]
23pub struct CollectionManager {
24 collections: DashMap<String, Arc<RwLock<Collection>>>,
26
27 aliases: DashMap<String, String>,
29
30 base_path: PathBuf,
32}
33
34impl CollectionManager {
35 pub fn new(base_path: PathBuf) -> Result<Self> {
50 std::fs::create_dir_all(&base_path)?;
52
53 let manager = Self {
54 collections: DashMap::new(),
55 aliases: DashMap::new(),
56 base_path,
57 };
58
59 manager.load_collections()?;
61
62 Ok(manager)
63 }
64
65 pub fn create_collection(&self, name: &str, config: CollectionConfig) -> Result<()> {
76 Self::validate_name(name)?;
78
79 if self.collections.contains_key(name) {
81 return Err(CollectionError::CollectionAlreadyExists {
82 name: name.to_string(),
83 });
84 }
85
86 if self.aliases.contains_key(name) {
88 return Err(CollectionError::InvalidName {
89 name: name.to_string(),
90 reason: "An alias with this name already exists".to_string(),
91 });
92 }
93
94 let storage_path = self.base_path.join(name);
96 std::fs::create_dir_all(&storage_path)?;
97
98 let db_path = storage_path
99 .join("vectors.db")
100 .to_string_lossy()
101 .to_string();
102
103 let collection = Collection::new(name.to_string(), config, db_path)?;
105
106 self.save_collection_metadata(&collection)?;
108
109 self.collections
111 .insert(name.to_string(), Arc::new(RwLock::new(collection)));
112
113 Ok(())
114 }
115
116 pub fn delete_collection(&self, name: &str) -> Result<()> {
127 if !self.collections.contains_key(name) {
129 return Err(CollectionError::CollectionNotFound {
130 name: name.to_string(),
131 });
132 }
133
134 let active_aliases: Vec<String> = self
136 .aliases
137 .iter()
138 .filter(|entry| entry.value() == name)
139 .map(|entry| entry.key().clone())
140 .collect();
141
142 if !active_aliases.is_empty() {
143 return Err(CollectionError::CollectionHasAliases {
144 collection: name.to_string(),
145 aliases: active_aliases,
146 });
147 }
148
149 self.collections.remove(name);
151
152 let collection_path = self.base_path.join(name);
154 if collection_path.exists() {
155 std::fs::remove_dir_all(&collection_path)?;
156 }
157
158 Ok(())
159 }
160
161 pub fn get_collection(&self, name: &str) -> Option<Arc<RwLock<Collection>>> {
167 let collection_name = self.resolve_alias(name).unwrap_or_else(|| name.to_string());
169
170 self.collections
171 .get(&collection_name)
172 .map(|entry| entry.value().clone())
173 }
174
175 pub fn list_collections(&self) -> Vec<String> {
177 self.collections
178 .iter()
179 .map(|entry| entry.key().clone())
180 .collect()
181 }
182
183 pub fn collection_exists(&self, name: &str) -> bool {
189 self.collections.contains_key(name)
190 }
191
192 pub fn collection_stats(&self, name: &str) -> Result<CollectionStats> {
194 let collection =
195 self.get_collection(name)
196 .ok_or_else(|| CollectionError::CollectionNotFound {
197 name: name.to_string(),
198 })?;
199
200 let guard = collection.read();
201 guard.stats()
202 }
203
204 pub fn create_alias(&self, alias: &str, collection: &str) -> Result<()> {
218 Self::validate_name(alias)?;
220
221 if self.aliases.contains_key(alias) {
223 return Err(CollectionError::AliasAlreadyExists {
224 alias: alias.to_string(),
225 });
226 }
227
228 if self.collections.contains_key(alias) {
230 return Err(CollectionError::InvalidName {
231 name: alias.to_string(),
232 reason: "A collection with this name already exists".to_string(),
233 });
234 }
235
236 if !self.collections.contains_key(collection) {
238 return Err(CollectionError::CollectionNotFound {
239 name: collection.to_string(),
240 });
241 }
242
243 self.aliases
245 .insert(alias.to_string(), collection.to_string());
246
247 self.save_aliases()?;
249
250 Ok(())
251 }
252
253 pub fn delete_alias(&self, alias: &str) -> Result<()> {
263 if self.aliases.remove(alias).is_none() {
264 return Err(CollectionError::AliasNotFound {
265 alias: alias.to_string(),
266 });
267 }
268
269 self.save_aliases()?;
271
272 Ok(())
273 }
274
275 pub fn switch_alias(&self, alias: &str, new_collection: &str) -> Result<()> {
287 if !self.aliases.contains_key(alias) {
289 return Err(CollectionError::AliasNotFound {
290 alias: alias.to_string(),
291 });
292 }
293
294 if !self.collections.contains_key(new_collection) {
296 return Err(CollectionError::CollectionNotFound {
297 name: new_collection.to_string(),
298 });
299 }
300
301 self.aliases
303 .insert(alias.to_string(), new_collection.to_string());
304
305 self.save_aliases()?;
307
308 Ok(())
309 }
310
311 pub fn resolve_alias(&self, name_or_alias: &str) -> Option<String> {
321 self.aliases
322 .get(name_or_alias)
323 .map(|entry| entry.value().clone())
324 }
325
326 pub fn list_aliases(&self) -> Vec<(String, String)> {
328 self.aliases
329 .iter()
330 .map(|entry| (entry.key().clone(), entry.value().clone()))
331 .collect()
332 }
333
334 pub fn is_alias(&self, name: &str) -> bool {
336 self.aliases.contains_key(name)
337 }
338
339 fn validate_name(name: &str) -> Result<()> {
343 if name.is_empty() {
344 return Err(CollectionError::InvalidName {
345 name: name.to_string(),
346 reason: "Name cannot be empty".to_string(),
347 });
348 }
349
350 if name.len() > 255 {
351 return Err(CollectionError::InvalidName {
352 name: name.to_string(),
353 reason: "Name too long (max 255 characters)".to_string(),
354 });
355 }
356
357 if !name
359 .chars()
360 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
361 {
362 return Err(CollectionError::InvalidName {
363 name: name.to_string(),
364 reason: "Name can only contain letters, numbers, hyphens, and underscores"
365 .to_string(),
366 });
367 }
368
369 Ok(())
370 }
371
372 fn load_collections(&self) -> Result<()> {
374 if !self.base_path.exists() {
375 return Ok(());
376 }
377
378 self.load_aliases()?;
380
381 for entry in std::fs::read_dir(&self.base_path)? {
383 let entry = entry?;
384 let path = entry.path();
385
386 if path.is_dir() {
387 let name = path
388 .file_name()
389 .and_then(|n| n.to_str())
390 .unwrap_or("")
391 .to_string();
392
393 if name.starts_with('.') || name == "aliases.json" {
395 continue;
396 }
397
398 if let Ok(metadata) = self.load_collection_metadata(&name) {
400 let db_path = path.join("vectors.db").to_string_lossy().to_string();
401
402 if let Ok(mut collection) =
404 Collection::new(metadata.name.clone(), metadata.config, db_path)
405 {
406 collection.created_at = metadata.created_at;
407 collection.updated_at = metadata.updated_at;
408
409 self.collections
410 .insert(name.clone(), Arc::new(RwLock::new(collection)));
411 }
412 }
413 }
414 }
415
416 Ok(())
417 }
418
419 fn save_collection_metadata(&self, collection: &Collection) -> Result<()> {
421 let metadata = CollectionMetadata {
422 name: collection.name.clone(),
423 config: collection.config.clone(),
424 created_at: collection.created_at,
425 updated_at: collection.updated_at,
426 };
427
428 let metadata_path = self.base_path.join(&collection.name).join("metadata.json");
429
430 let json = serde_json::to_string_pretty(&metadata)?;
431 std::fs::write(metadata_path, json)?;
432
433 Ok(())
434 }
435
436 fn load_collection_metadata(&self, name: &str) -> Result<CollectionMetadata> {
438 let metadata_path = self.base_path.join(name).join("metadata.json");
439 let json = std::fs::read_to_string(metadata_path)?;
440 let metadata: CollectionMetadata = serde_json::from_str(&json)?;
441 Ok(metadata)
442 }
443
444 fn save_aliases(&self) -> Result<()> {
446 let aliases: HashMap<String, String> = self
447 .aliases
448 .iter()
449 .map(|entry| (entry.key().clone(), entry.value().clone()))
450 .collect();
451
452 let aliases_path = self.base_path.join("aliases.json");
453 let json = serde_json::to_string_pretty(&aliases)?;
454 std::fs::write(aliases_path, json)?;
455
456 Ok(())
457 }
458
459 fn load_aliases(&self) -> Result<()> {
461 let aliases_path = self.base_path.join("aliases.json");
462
463 if !aliases_path.exists() {
464 return Ok(());
465 }
466
467 let json = std::fs::read_to_string(aliases_path)?;
468 let aliases: HashMap<String, String> = serde_json::from_str(&json)?;
469
470 for (alias, collection) in aliases {
471 self.aliases.insert(alias, collection);
472 }
473
474 Ok(())
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 fn fresh_manager(suffix: &str) -> (CollectionManager, PathBuf) {
484 let temp_dir = std::env::temp_dir().join(format!("ruvector_mgr_test_{}", suffix));
485 let _ = std::fs::remove_dir_all(&temp_dir);
486 let manager = CollectionManager::new(temp_dir.clone()).unwrap();
487 (manager, temp_dir)
488 }
489
490 fn cleanup(path: &PathBuf) {
491 let _ = std::fs::remove_dir_all(path);
492 }
493
494 #[test]
497 fn test_validate_name() {
498 assert!(CollectionManager::validate_name("valid-name_123").is_ok());
499 assert!(CollectionManager::validate_name("").is_err());
500 assert!(CollectionManager::validate_name("invalid name").is_err());
501 assert!(CollectionManager::validate_name("invalid/name").is_err());
502 }
503
504 #[test]
505 fn test_validate_name_max_length() {
506 let long_name = "a".repeat(255);
507 assert!(CollectionManager::validate_name(&long_name).is_ok());
508
509 let too_long = "a".repeat(256);
510 assert!(CollectionManager::validate_name(&too_long).is_err());
511 }
512
513 #[test]
514 fn test_validate_name_special_characters_rejected() {
515 assert!(CollectionManager::validate_name("name.with.dots").is_err());
516 assert!(CollectionManager::validate_name("name@symbol").is_err());
517 assert!(CollectionManager::validate_name("name#hash").is_err());
518 assert!(CollectionManager::validate_name("has spaces").is_err());
519 }
520
521 #[test]
522 fn test_validate_name_valid_chars() {
523 assert!(CollectionManager::validate_name("abc").is_ok());
524 assert!(CollectionManager::validate_name("ABC").is_ok());
525 assert!(CollectionManager::validate_name("123").is_ok());
526 assert!(CollectionManager::validate_name("a-b").is_ok());
527 assert!(CollectionManager::validate_name("a_b").is_ok());
528 assert!(CollectionManager::validate_name("a-b_c-123").is_ok());
529 }
530
531 #[test]
534 fn test_collection_manager() -> Result<()> {
535 let temp_dir = std::env::temp_dir().join("ruvector_test_collections");
536 let _ = std::fs::remove_dir_all(&temp_dir);
537
538 let manager = CollectionManager::new(temp_dir.clone())?;
539
540 let config = CollectionConfig::with_dimensions(128);
542 manager.create_collection("test", config)?;
543
544 assert!(manager.collection_exists("test"));
545 assert_eq!(manager.list_collections().len(), 1);
546
547 manager.create_alias("test_alias", "test")?;
549 assert!(manager.is_alias("test_alias"));
550 assert_eq!(
551 manager.resolve_alias("test_alias"),
552 Some("test".to_string())
553 );
554
555 assert!(manager.get_collection("test_alias").is_some());
557
558 manager.delete_alias("test_alias")?;
560 manager.delete_collection("test")?;
561 let _ = std::fs::remove_dir_all(&temp_dir);
562
563 Ok(())
564 }
565
566 #[test]
569 fn test_create_duplicate_collection_returns_error() {
570 let (manager, temp) = fresh_manager("dup_coll");
571 let config = CollectionConfig::with_dimensions(32);
572 manager.create_collection("docs", config.clone()).unwrap();
573
574 let err = manager.create_collection("docs", config).unwrap_err();
575 match err {
576 CollectionError::CollectionAlreadyExists { name } => {
577 assert_eq!(name, "docs");
578 }
579 other => panic!("Expected CollectionAlreadyExists, got: {:?}", other),
580 }
581
582 cleanup(&temp);
583 }
584
585 #[test]
586 fn test_create_collection_with_alias_name_returns_error() {
587 let (manager, temp) = fresh_manager("coll_alias_clash");
588 let config = CollectionConfig::with_dimensions(32);
589 manager.create_collection("real", config.clone()).unwrap();
590 manager.create_alias("taken", "real").unwrap();
591
592 let err = manager.create_collection("taken", config).unwrap_err();
593 match err {
594 CollectionError::InvalidName { name, .. } => {
595 assert_eq!(name, "taken");
596 }
597 other => panic!("Expected InvalidName, got: {:?}", other),
598 }
599
600 cleanup(&temp);
601 }
602
603 #[test]
606 fn test_delete_nonexistent_collection_returns_error() {
607 let (manager, temp) = fresh_manager("del_nonexist");
608
609 let err = manager.delete_collection("ghost").unwrap_err();
610 match err {
611 CollectionError::CollectionNotFound { name } => {
612 assert_eq!(name, "ghost");
613 }
614 other => panic!("Expected CollectionNotFound, got: {:?}", other),
615 }
616
617 cleanup(&temp);
618 }
619
620 #[test]
621 fn test_delete_collection_with_aliases_returns_error() {
622 let (manager, temp) = fresh_manager("del_has_alias");
623 let config = CollectionConfig::with_dimensions(32);
624 manager.create_collection("coll_a", config).unwrap();
625 manager.create_alias("alias_a", "coll_a").unwrap();
626
627 let err = manager.delete_collection("coll_a").unwrap_err();
628 match err {
629 CollectionError::CollectionHasAliases {
630 collection,
631 aliases,
632 } => {
633 assert_eq!(collection, "coll_a");
634 assert!(aliases.contains(&"alias_a".to_string()));
635 }
636 other => panic!("Expected CollectionHasAliases, got: {:?}", other),
637 }
638
639 cleanup(&temp);
640 }
641
642 #[test]
645 fn test_get_nonexistent_collection_returns_none() {
646 let (manager, temp) = fresh_manager("get_none");
647 assert!(manager.get_collection("nope").is_none());
648 cleanup(&temp);
649 }
650
651 #[test]
652 fn test_get_collection_by_direct_name() {
653 let (manager, temp) = fresh_manager("get_direct");
654 let config = CollectionConfig::with_dimensions(64);
655 manager.create_collection("my_coll", config).unwrap();
656
657 let coll = manager.get_collection("my_coll");
658 assert!(coll.is_some());
659
660 cleanup(&temp);
661 }
662
663 #[test]
666 fn test_list_collections_empty() {
667 let (manager, temp) = fresh_manager("list_empty");
668 assert!(manager.list_collections().is_empty());
669 cleanup(&temp);
670 }
671
672 #[test]
673 fn test_list_collections_multiple() {
674 let (manager, temp) = fresh_manager("list_multi");
675 let config = CollectionConfig::with_dimensions(16);
676 manager.create_collection("alpha", config.clone()).unwrap();
677 manager.create_collection("beta", config).unwrap();
678
679 let names = manager.list_collections();
680 assert_eq!(names.len(), 2);
681 assert!(names.contains(&"alpha".to_string()));
682 assert!(names.contains(&"beta".to_string()));
683
684 cleanup(&temp);
685 }
686
687 #[test]
688 fn test_collection_exists_false_for_missing() {
689 let (manager, temp) = fresh_manager("exists_false");
690 assert!(!manager.collection_exists("nothing"));
691 cleanup(&temp);
692 }
693
694 #[test]
697 fn test_collection_stats_for_empty_collection() {
698 let (manager, temp) = fresh_manager("stats_empty");
699 let config = CollectionConfig::with_dimensions(16);
700 manager.create_collection("stats_c", config).unwrap();
701
702 let stats = manager.collection_stats("stats_c").unwrap();
703 assert_eq!(stats.vectors_count, 0);
704 assert!(stats.is_empty());
705
706 cleanup(&temp);
707 }
708
709 #[test]
710 fn test_collection_stats_nonexistent_returns_error() {
711 let (manager, temp) = fresh_manager("stats_noexist");
712
713 let err = manager.collection_stats("missing").unwrap_err();
714 match err {
715 CollectionError::CollectionNotFound { name } => {
716 assert_eq!(name, "missing");
717 }
718 other => panic!("Expected CollectionNotFound, got: {:?}", other),
719 }
720
721 cleanup(&temp);
722 }
723
724 #[test]
727 fn test_create_alias_for_nonexistent_collection_returns_error() {
728 let (manager, temp) = fresh_manager("alias_noexist");
729
730 let err = manager.create_alias("my_alias", "ghost_coll").unwrap_err();
731 match err {
732 CollectionError::CollectionNotFound { name } => {
733 assert_eq!(name, "ghost_coll");
734 }
735 other => panic!("Expected CollectionNotFound, got: {:?}", other),
736 }
737
738 cleanup(&temp);
739 }
740
741 #[test]
742 fn test_create_duplicate_alias_returns_error() {
743 let (manager, temp) = fresh_manager("alias_dup");
744 let config = CollectionConfig::with_dimensions(16);
745 manager.create_collection("coll", config).unwrap();
746 manager.create_alias("alias1", "coll").unwrap();
747
748 let err = manager.create_alias("alias1", "coll").unwrap_err();
749 match err {
750 CollectionError::AliasAlreadyExists { alias } => {
751 assert_eq!(alias, "alias1");
752 }
753 other => panic!("Expected AliasAlreadyExists, got: {:?}", other),
754 }
755
756 cleanup(&temp);
757 }
758
759 #[test]
760 fn test_create_alias_with_collection_name_returns_error() {
761 let (manager, temp) = fresh_manager("alias_name_clash");
762 let config = CollectionConfig::with_dimensions(16);
763 manager.create_collection("coll", config.clone()).unwrap();
764 manager.create_collection("coll2", config).unwrap();
765
766 let err = manager.create_alias("coll", "coll2").unwrap_err();
767 match err {
768 CollectionError::InvalidName { name, .. } => {
769 assert_eq!(name, "coll");
770 }
771 other => panic!("Expected InvalidName, got: {:?}", other),
772 }
773
774 cleanup(&temp);
775 }
776
777 #[test]
778 fn test_delete_alias_nonexistent_returns_error() {
779 let (manager, temp) = fresh_manager("del_alias_none");
780
781 let err = manager.delete_alias("no_such_alias").unwrap_err();
782 match err {
783 CollectionError::AliasNotFound { alias } => {
784 assert_eq!(alias, "no_such_alias");
785 }
786 other => panic!("Expected AliasNotFound, got: {:?}", other),
787 }
788
789 cleanup(&temp);
790 }
791
792 #[test]
793 fn test_list_aliases_empty() {
794 let (manager, temp) = fresh_manager("aliases_empty");
795 assert!(manager.list_aliases().is_empty());
796 cleanup(&temp);
797 }
798
799 #[test]
800 fn test_list_aliases_returns_all() {
801 let (manager, temp) = fresh_manager("aliases_list");
802 let config = CollectionConfig::with_dimensions(16);
803 manager.create_collection("c1", config.clone()).unwrap();
804 manager.create_collection("c2", config).unwrap();
805 manager.create_alias("a1", "c1").unwrap();
806 manager.create_alias("a2", "c2").unwrap();
807
808 let aliases = manager.list_aliases();
809 assert_eq!(aliases.len(), 2);
810
811 let alias_names: Vec<&String> = aliases.iter().map(|(a, _)| a).collect();
812 assert!(alias_names.contains(&&"a1".to_string()));
813 assert!(alias_names.contains(&&"a2".to_string()));
814
815 cleanup(&temp);
816 }
817
818 #[test]
819 fn test_is_alias_returns_false_for_non_alias() {
820 let (manager, temp) = fresh_manager("is_alias_false");
821 assert!(!manager.is_alias("random"));
822 cleanup(&temp);
823 }
824
825 #[test]
826 fn test_resolve_alias_returns_none_for_non_alias() {
827 let (manager, temp) = fresh_manager("resolve_none");
828 assert!(manager.resolve_alias("not_an_alias").is_none());
829 cleanup(&temp);
830 }
831
832 #[test]
835 fn test_switch_alias_success() {
836 let (manager, temp) = fresh_manager("switch_ok");
837 let config = CollectionConfig::with_dimensions(16);
838 manager.create_collection("c1", config.clone()).unwrap();
839 manager.create_collection("c2", config).unwrap();
840 manager.create_alias("prod", "c1").unwrap();
841
842 assert_eq!(manager.resolve_alias("prod"), Some("c1".to_string()));
843
844 manager.switch_alias("prod", "c2").unwrap();
845 assert_eq!(manager.resolve_alias("prod"), Some("c2".to_string()));
846
847 cleanup(&temp);
848 }
849
850 #[test]
851 fn test_switch_alias_nonexistent_alias_returns_error() {
852 let (manager, temp) = fresh_manager("switch_no_alias");
853 let config = CollectionConfig::with_dimensions(16);
854 manager.create_collection("c1", config).unwrap();
855
856 let err = manager.switch_alias("ghost_alias", "c1").unwrap_err();
857 match err {
858 CollectionError::AliasNotFound { alias } => {
859 assert_eq!(alias, "ghost_alias");
860 }
861 other => panic!("Expected AliasNotFound, got: {:?}", other),
862 }
863
864 cleanup(&temp);
865 }
866
867 #[test]
868 fn test_switch_alias_nonexistent_collection_returns_error() {
869 let (manager, temp) = fresh_manager("switch_no_coll");
870 let config = CollectionConfig::with_dimensions(16);
871 manager.create_collection("c1", config).unwrap();
872 manager.create_alias("prod", "c1").unwrap();
873
874 let err = manager.switch_alias("prod", "ghost_coll").unwrap_err();
875 match err {
876 CollectionError::CollectionNotFound { name } => {
877 assert_eq!(name, "ghost_coll");
878 }
879 other => panic!("Expected CollectionNotFound, got: {:?}", other),
880 }
881
882 cleanup(&temp);
883 }
884
885 #[test]
888 fn test_full_lifecycle() -> Result<()> {
889 let (manager, temp) = fresh_manager("lifecycle");
890 let config = CollectionConfig::with_dimensions(32);
891
892 manager.create_collection("v1", config.clone())?;
894 manager.create_collection("v2", config)?;
895 assert_eq!(manager.list_collections().len(), 2);
896
897 manager.create_alias("current", "v1")?;
899 assert!(manager.get_collection("current").is_some());
900
901 manager.switch_alias("current", "v2")?;
903 assert_eq!(manager.resolve_alias("current"), Some("v2".to_string()));
904
905 manager.delete_alias("current")?;
907 assert!(!manager.is_alias("current"));
908
909 manager.delete_collection("v1")?;
911 manager.delete_collection("v2")?;
912 assert!(manager.list_collections().is_empty());
913
914 cleanup(&temp);
915 Ok(())
916 }
917}