Skip to main content

ruvector_collections/
manager.rs

1//! Collection manager for multi-collection operations
2
3use 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/// Metadata for persisting collections
13#[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/// Manages multiple vector collections with alias support
22#[derive(Debug)]
23pub struct CollectionManager {
24    /// Active collections
25    collections: DashMap<String, Arc<RwLock<Collection>>>,
26
27    /// Alias mappings (alias -> collection_name)
28    aliases: DashMap<String, String>,
29
30    /// Base path for storing collections
31    base_path: PathBuf,
32}
33
34impl CollectionManager {
35    /// Create a new collection manager
36    ///
37    /// # Arguments
38    ///
39    /// * `base_path` - Directory where collections will be stored
40    ///
41    /// # Example
42    ///
43    /// ```no_run
44    /// use ruvector_collections::CollectionManager;
45    /// use std::path::PathBuf;
46    ///
47    /// let manager = CollectionManager::new(PathBuf::from("./collections")).unwrap();
48    /// ```
49    pub fn new(base_path: PathBuf) -> Result<Self> {
50        // Create base directory if it doesn't exist
51        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        // Load existing collections
60        manager.load_collections()?;
61
62        Ok(manager)
63    }
64
65    /// Create a new collection
66    ///
67    /// # Arguments
68    ///
69    /// * `name` - Collection name (must be unique)
70    /// * `config` - Collection configuration
71    ///
72    /// # Errors
73    ///
74    /// Returns `CollectionAlreadyExists` if a collection with the same name exists
75    pub fn create_collection(&self, name: &str, config: CollectionConfig) -> Result<()> {
76        // Validate collection name
77        Self::validate_name(name)?;
78
79        // Check if collection already exists
80        if self.collections.contains_key(name) {
81            return Err(CollectionError::CollectionAlreadyExists {
82                name: name.to_string(),
83            });
84        }
85
86        // Check if an alias with this name exists
87        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        // Create storage path for this collection
95        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        // Create collection
104        let collection = Collection::new(name.to_string(), config, db_path)?;
105
106        // Save metadata
107        self.save_collection_metadata(&collection)?;
108
109        // Add to collections map
110        self.collections
111            .insert(name.to_string(), Arc::new(RwLock::new(collection)));
112
113        Ok(())
114    }
115
116    /// Delete a collection
117    ///
118    /// # Arguments
119    ///
120    /// * `name` - Collection name to delete
121    ///
122    /// # Errors
123    ///
124    /// Returns `CollectionNotFound` if collection doesn't exist
125    /// Returns `CollectionHasAliases` if collection has active aliases
126    pub fn delete_collection(&self, name: &str) -> Result<()> {
127        // Check if collection exists
128        if !self.collections.contains_key(name) {
129            return Err(CollectionError::CollectionNotFound {
130                name: name.to_string(),
131            });
132        }
133
134        // Check for active aliases
135        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        // Remove from collections map
150        self.collections.remove(name);
151
152        // Delete from disk
153        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    /// Get a collection by name or alias
162    ///
163    /// # Arguments
164    ///
165    /// * `name` - Collection name or alias
166    pub fn get_collection(&self, name: &str) -> Option<Arc<RwLock<Collection>>> {
167        // Try to resolve as alias first
168        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    /// List all collection names
176    pub fn list_collections(&self) -> Vec<String> {
177        self.collections
178            .iter()
179            .map(|entry| entry.key().clone())
180            .collect()
181    }
182
183    /// Check if a collection exists
184    ///
185    /// # Arguments
186    ///
187    /// * `name` - Collection name (not alias)
188    pub fn collection_exists(&self, name: &str) -> bool {
189        self.collections.contains_key(name)
190    }
191
192    /// Get statistics for a collection
193    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    // ===== Alias Management =====
205
206    /// Create an alias for a collection
207    ///
208    /// # Arguments
209    ///
210    /// * `alias` - Alias name (must be unique)
211    /// * `collection` - Target collection name
212    ///
213    /// # Errors
214    ///
215    /// Returns `AliasAlreadyExists` if alias already exists
216    /// Returns `CollectionNotFound` if target collection doesn't exist
217    pub fn create_alias(&self, alias: &str, collection: &str) -> Result<()> {
218        // Validate alias name
219        Self::validate_name(alias)?;
220
221        // Check if alias already exists
222        if self.aliases.contains_key(alias) {
223            return Err(CollectionError::AliasAlreadyExists {
224                alias: alias.to_string(),
225            });
226        }
227
228        // Check if a collection with this name exists
229        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        // Verify target collection exists
237        if !self.collections.contains_key(collection) {
238            return Err(CollectionError::CollectionNotFound {
239                name: collection.to_string(),
240            });
241        }
242
243        // Create alias
244        self.aliases
245            .insert(alias.to_string(), collection.to_string());
246
247        // Save aliases
248        self.save_aliases()?;
249
250        Ok(())
251    }
252
253    /// Delete an alias
254    ///
255    /// # Arguments
256    ///
257    /// * `alias` - Alias name to delete
258    ///
259    /// # Errors
260    ///
261    /// Returns `AliasNotFound` if alias doesn't exist
262    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        // Save aliases
270        self.save_aliases()?;
271
272        Ok(())
273    }
274
275    /// Switch an alias to point to a different collection
276    ///
277    /// # Arguments
278    ///
279    /// * `alias` - Alias name
280    /// * `new_collection` - New target collection name
281    ///
282    /// # Errors
283    ///
284    /// Returns `AliasNotFound` if alias doesn't exist
285    /// Returns `CollectionNotFound` if new collection doesn't exist
286    pub fn switch_alias(&self, alias: &str, new_collection: &str) -> Result<()> {
287        // Verify alias exists
288        if !self.aliases.contains_key(alias) {
289            return Err(CollectionError::AliasNotFound {
290                alias: alias.to_string(),
291            });
292        }
293
294        // Verify new collection exists
295        if !self.collections.contains_key(new_collection) {
296            return Err(CollectionError::CollectionNotFound {
297                name: new_collection.to_string(),
298            });
299        }
300
301        // Update alias
302        self.aliases
303            .insert(alias.to_string(), new_collection.to_string());
304
305        // Save aliases
306        self.save_aliases()?;
307
308        Ok(())
309    }
310
311    /// Resolve an alias to a collection name
312    ///
313    /// # Arguments
314    ///
315    /// * `name_or_alias` - Collection name or alias
316    ///
317    /// # Returns
318    ///
319    /// `Some(collection_name)` if it's an alias, `None` if it's not an alias
320    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    /// List all aliases with their target collections
327    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    /// Check if a name is an alias
335    pub fn is_alias(&self, name: &str) -> bool {
336        self.aliases.contains_key(name)
337    }
338
339    // ===== Internal Methods =====
340
341    /// Validate a collection or alias name
342    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        // Only allow alphanumeric, hyphens, underscores
358        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    /// Load existing collections from disk
373    fn load_collections(&self) -> Result<()> {
374        if !self.base_path.exists() {
375            return Ok(());
376        }
377
378        // Load aliases
379        self.load_aliases()?;
380
381        // Scan for collection directories
382        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                // Skip special directories
394                if name.starts_with('.') || name == "aliases.json" {
395                    continue;
396                }
397
398                // Try to load collection metadata
399                if let Ok(metadata) = self.load_collection_metadata(&name) {
400                    let db_path = path.join("vectors.db").to_string_lossy().to_string();
401
402                    // Recreate collection
403                    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    /// Save collection metadata to disk
420    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    /// Load collection metadata from disk
437    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    /// Save aliases to disk
445    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    /// Load aliases from disk
460    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    // Helper to create a fresh manager in a unique temp directory
483    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    // ===== Name validation tests =====
495
496    #[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    // ===== Basic collection lifecycle =====
532
533    #[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        // Create collection
541        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        // Create alias
548        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        // Get collection by alias
556        assert!(manager.get_collection("test_alias").is_some());
557
558        // Cleanup
559        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    // ===== Create collection error paths =====
567
568    #[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    // ===== Delete collection error paths =====
604
605    #[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    // ===== Get collection tests =====
643
644    #[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    // ===== Collection exists / list tests =====
664
665    #[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    // ===== Collection stats tests =====
695
696    #[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    // ===== Alias management tests =====
725
726    #[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    // ===== Switch alias tests =====
833
834    #[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    // ===== Full lifecycle: create, alias, switch, delete =====
886
887    #[test]
888    fn test_full_lifecycle() -> Result<()> {
889        let (manager, temp) = fresh_manager("lifecycle");
890        let config = CollectionConfig::with_dimensions(32);
891
892        // Create two collections
893        manager.create_collection("v1", config.clone())?;
894        manager.create_collection("v2", config)?;
895        assert_eq!(manager.list_collections().len(), 2);
896
897        // Alias to v1
898        manager.create_alias("current", "v1")?;
899        assert!(manager.get_collection("current").is_some());
900
901        // Switch alias to v2
902        manager.switch_alias("current", "v2")?;
903        assert_eq!(manager.resolve_alias("current"), Some("v2".to_string()));
904
905        // Delete the alias
906        manager.delete_alias("current")?;
907        assert!(!manager.is_alias("current"));
908
909        // Delete collections
910        manager.delete_collection("v1")?;
911        manager.delete_collection("v2")?;
912        assert!(manager.list_collections().is_empty());
913
914        cleanup(&temp);
915        Ok(())
916    }
917}