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    #[test]
483    fn test_validate_name() {
484        assert!(CollectionManager::validate_name("valid-name_123").is_ok());
485        assert!(CollectionManager::validate_name("").is_err());
486        assert!(CollectionManager::validate_name("invalid name").is_err());
487        assert!(CollectionManager::validate_name("invalid/name").is_err());
488    }
489
490    #[test]
491    fn test_collection_manager() -> Result<()> {
492        let temp_dir = std::env::temp_dir().join("ruvector_test_collections");
493        let _ = std::fs::remove_dir_all(&temp_dir);
494
495        let manager = CollectionManager::new(temp_dir.clone())?;
496
497        // Create collection
498        let config = CollectionConfig::with_dimensions(128);
499        manager.create_collection("test", config)?;
500
501        assert!(manager.collection_exists("test"));
502        assert_eq!(manager.list_collections().len(), 1);
503
504        // Create alias
505        manager.create_alias("test_alias", "test")?;
506        assert!(manager.is_alias("test_alias"));
507        assert_eq!(
508            manager.resolve_alias("test_alias"),
509            Some("test".to_string())
510        );
511
512        // Get collection by alias
513        assert!(manager.get_collection("test_alias").is_some());
514
515        // Cleanup
516        manager.delete_alias("test_alias")?;
517        manager.delete_collection("test")?;
518        let _ = std::fs::remove_dir_all(&temp_dir);
519
520        Ok(())
521    }
522}