Skip to main content

aida_core/db/
traits.rs

1//! Database abstraction traits
2//!
3//! This module defines the core trait that all storage backends must implement.
4
5use anyhow::Result;
6use std::path::PathBuf;
7use uuid::Uuid;
8
9use crate::models::{QueueEntry, Requirement, RequirementsStore, User};
10
11/// Error type for version conflicts during optimistic locking
12#[derive(Debug, Clone)]
13pub struct VersionConflict {
14    /// ID of the conflicting record
15    pub id: Uuid,
16    /// The version the client expected
17    pub expected_version: i64,
18    /// The current version in the database
19    pub current_version: i64,
20    /// Human-readable identifier (spec_id or name)
21    pub display_id: String,
22}
23
24impl std::fmt::Display for VersionConflict {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(
27            f,
28            "Version conflict for {}: expected version {}, but current version is {}. \
29             Another user may have modified this record.",
30            self.display_id, self.expected_version, self.current_version
31        )
32    }
33}
34
35impl std::error::Error for VersionConflict {}
36
37/// Result type for optimistic locking operations
38#[derive(Debug)]
39pub enum UpdateResult {
40    /// Update succeeded
41    Success,
42    /// Update failed due to version conflict
43    Conflict(VersionConflict),
44}
45
46/// Types of database backends available
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum BackendType {
49    /// YAML file storage (single file)
50    Yaml,
51    /// SQLite database storage
52    Sqlite,
53    /// PostgreSQL database storage
54    Postgres,
55    /// Git-backed storage (sharded YAML files in a directory)
56    Git,
57}
58
59impl std::fmt::Display for BackendType {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            BackendType::Yaml => write!(f, "YAML"),
63            BackendType::Sqlite => write!(f, "SQLite"),
64            BackendType::Postgres => write!(f, "PostgreSQL"),
65            BackendType::Git => write!(f, "Git"),
66        }
67    }
68}
69
70/// Configuration for database backends
71#[derive(Debug, Clone)]
72pub struct DatabaseConfig {
73    /// Path to the database file
74    pub path: PathBuf,
75    /// Backend type
76    pub backend_type: BackendType,
77    /// Whether to enable write-ahead logging (SQLite only)
78    pub wal_mode: bool,
79}
80
81impl Default for DatabaseConfig {
82    fn default() -> Self {
83        Self {
84            path: PathBuf::from("requirements.yaml"),
85            backend_type: BackendType::Yaml,
86            wal_mode: true,
87        }
88    }
89}
90
91/// Core trait for database backends
92///
93/// This trait provides a unified interface for storing and retrieving
94/// requirements data, regardless of the underlying storage mechanism.
95///
96/// The design philosophy is:
97/// - `load()` and `save()` work with the full `RequirementsStore` for compatibility
98/// - Individual CRUD operations are provided for more efficient database access
99/// - Backends can choose to implement efficient versions or delegate to load/save
100pub trait DatabaseBackend: Send + Sync {
101    /// Returns the backend type
102    fn backend_type(&self) -> BackendType;
103
104    /// Returns the path to the database file
105    fn path(&self) -> &std::path::Path;
106
107    // =========================================================================
108    // Full Store Operations (for compatibility with existing code)
109    // =========================================================================
110
111    /// Loads the entire requirements store from the database
112    fn load(&self) -> Result<RequirementsStore>;
113
114    /// Saves the entire requirements store to the database
115    fn save(&self, store: &RequirementsStore) -> Result<()>;
116
117    /// Performs an atomic update operation
118    /// Default implementation loads, applies changes, and saves
119    fn update_atomically<F>(&self, update_fn: F) -> Result<RequirementsStore>
120    where
121        F: FnOnce(&mut RequirementsStore),
122        Self: Sized,
123    {
124        let mut store = self.load()?;
125        update_fn(&mut store);
126        self.save(&store)?;
127        Ok(store)
128    }
129
130    // =========================================================================
131    // Requirement CRUD Operations
132    // =========================================================================
133
134    /// Gets a requirement by its UUID
135    fn get_requirement(&self, id: &Uuid) -> Result<Option<Requirement>> {
136        let store = self.load()?;
137        Ok(store.requirements.iter().find(|r| &r.id == id).cloned())
138    }
139
140    /// Gets a requirement by its spec_id (e.g., "FR-001")
141    fn get_requirement_by_spec_id(&self, spec_id: &str) -> Result<Option<Requirement>> {
142        let store = self.load()?;
143        Ok(store
144            .requirements
145            .iter()
146            .find(|r| r.spec_id.as_deref() == Some(spec_id))
147            .cloned())
148    }
149
150    /// Lists all requirements (non-archived by default)
151    fn list_requirements(&self, include_archived: bool) -> Result<Vec<Requirement>> {
152        let store = self.load()?;
153        Ok(store
154            .requirements
155            .iter()
156            .filter(|r| include_archived || !r.archived)
157            .cloned()
158            .collect())
159    }
160
161    /// Adds a new requirement
162    /// Returns the requirement with assigned spec_id
163    /// Note: This uses the simple SPEC-XXX format for ID generation.
164    /// For more complex ID generation (with feature/type prefixes), use update_atomically
165    fn add_requirement(&self, requirement: Requirement) -> Result<Requirement> {
166        let mut store = self.load()?;
167        let mut req = requirement;
168
169        // Assign spec_id if not set using the simple format
170        if req.spec_id.is_none() {
171            req.spec_id = Some(format!("SPEC-{:03}", store.next_spec_number));
172            store.next_spec_number += 1;
173        }
174
175        store.requirements.push(req.clone());
176        self.save(&store)?;
177        Ok(req)
178    }
179
180    /// Updates an existing requirement
181    fn update_requirement(&self, requirement: &Requirement) -> Result<()> {
182        let mut store = self.load()?;
183        if let Some(pos) = store
184            .requirements
185            .iter()
186            .position(|r| r.id == requirement.id)
187        {
188            store.requirements[pos] = requirement.clone();
189            self.save(&store)?;
190            Ok(())
191        } else {
192            anyhow::bail!("Requirement not found: {}", requirement.id)
193        }
194    }
195
196    /// Updates a requirement with optimistic locking
197    ///
198    /// This method checks that the requirement's version matches the database version
199    /// before updating. If another process modified the requirement, returns a conflict.
200    ///
201    /// The requirement's version field should contain the version that was loaded.
202    /// On success, the version is incremented in the database.
203    fn update_requirement_versioned(&self, requirement: &Requirement) -> Result<UpdateResult> {
204        // Default implementation doesn't support versioning, just updates
205        self.update_requirement(requirement)?;
206        Ok(UpdateResult::Success)
207    }
208
209    /// Deletes a requirement by UUID
210    fn delete_requirement(&self, id: &Uuid) -> Result<()> {
211        let mut store = self.load()?;
212        let original_len = store.requirements.len();
213        store.requirements.retain(|r| &r.id != id);
214        if store.requirements.len() == original_len {
215            anyhow::bail!("Requirement not found: {}", id)
216        }
217        self.save(&store)
218    }
219
220    // =========================================================================
221    // User CRUD Operations
222    // =========================================================================
223
224    /// Gets a user by UUID
225    fn get_user(&self, id: &Uuid) -> Result<Option<User>> {
226        let store = self.load()?;
227        Ok(store.users.iter().find(|u| &u.id == id).cloned())
228    }
229
230    /// Gets a user by handle
231    fn get_user_by_handle(&self, handle: &str) -> Result<Option<User>> {
232        let store = self.load()?;
233        Ok(store.users.iter().find(|u| u.handle == handle).cloned())
234    }
235
236    /// Lists all users
237    fn list_users(&self, include_archived: bool) -> Result<Vec<User>> {
238        let store = self.load()?;
239        Ok(store
240            .users
241            .iter()
242            .filter(|u| include_archived || !u.archived)
243            .cloned()
244            .collect())
245    }
246
247    /// Adds a new user
248    fn add_user(&self, user: User) -> Result<User> {
249        let mut store = self.load()?;
250        let mut u = user;
251
252        // Assign spec_id if not set
253        if u.spec_id.is_none() {
254            u.spec_id = Some(store.next_meta_id(crate::models::META_PREFIX_USER));
255        }
256
257        store.users.push(u.clone());
258        self.save(&store)?;
259        Ok(u)
260    }
261
262    /// Updates an existing user
263    fn update_user(&self, user: &User) -> Result<()> {
264        let mut store = self.load()?;
265        if let Some(pos) = store.users.iter().position(|u| u.id == user.id) {
266            store.users[pos] = user.clone();
267            self.save(&store)?;
268            Ok(())
269        } else {
270            anyhow::bail!("User not found: {}", user.id)
271        }
272    }
273
274    /// Deletes a user by UUID
275    fn delete_user(&self, id: &Uuid) -> Result<()> {
276        let mut store = self.load()?;
277        let original_len = store.users.len();
278        store.users.retain(|u| &u.id != id);
279        if store.users.len() == original_len {
280            anyhow::bail!("User not found: {}", id)
281        }
282        self.save(&store)
283    }
284
285    // =========================================================================
286    // Metadata Operations
287    // =========================================================================
288
289    /// Gets the database name
290    fn get_name(&self) -> Result<String> {
291        Ok(self.load()?.name)
292    }
293
294    /// Sets the database name
295    fn set_name(&self, name: &str) -> Result<()> {
296        let mut store = self.load()?;
297        store.name = name.to_string();
298        self.save(&store)
299    }
300
301    /// Gets the database title
302    fn get_title(&self) -> Result<String> {
303        Ok(self.load()?.title)
304    }
305
306    /// Sets the database title
307    fn set_title(&self, title: &str) -> Result<()> {
308        let mut store = self.load()?;
309        store.title = title.to_string();
310        self.save(&store)
311    }
312
313    /// Gets the database description
314    fn get_description(&self) -> Result<String> {
315        Ok(self.load()?.description)
316    }
317
318    /// Sets the database description
319    fn set_description(&self, description: &str) -> Result<()> {
320        let mut store = self.load()?;
321        store.description = description.to_string();
322        self.save(&store)
323    }
324
325    // =========================================================================
326    // Baseline Operations
327    // =========================================================================
328
329    /// Creates a new baseline from current requirements
330    /// For YAML backend, this also creates a git tag
331    fn create_baseline(
332        &self,
333        name: String,
334        description: Option<String>,
335        created_by: String,
336    ) -> Result<crate::models::Baseline> {
337        let mut store = self.load()?;
338        let baseline = store.create_baseline(name, description, created_by).clone();
339        self.save(&store)?;
340        Ok(baseline)
341    }
342
343    /// Lists all baselines
344    fn list_baselines(&self) -> Result<Vec<crate::models::Baseline>> {
345        let store = self.load()?;
346        Ok(store.baselines.clone())
347    }
348
349    /// Gets a baseline by ID
350    fn get_baseline(&self, id: &Uuid) -> Result<Option<crate::models::Baseline>> {
351        let store = self.load()?;
352        Ok(store.baselines.iter().find(|b| &b.id == id).cloned())
353    }
354
355    /// Deletes a baseline (if not locked)
356    fn delete_baseline(&self, id: &Uuid) -> Result<bool> {
357        let mut store = self.load()?;
358        let deleted = store.delete_baseline(id);
359        if deleted {
360            self.save(&store)?;
361        }
362        Ok(deleted)
363    }
364
365    /// Compares current state against a baseline
366    fn compare_with_baseline(
367        &self,
368        baseline_id: &Uuid,
369    ) -> Result<Option<crate::models::BaselineComparison>> {
370        let store = self.load()?;
371        Ok(store.compare_with_baseline(baseline_id))
372    }
373
374    /// Compares two baselines
375    fn compare_baselines(
376        &self,
377        source_id: &Uuid,
378        target_id: &Uuid,
379    ) -> Result<Option<crate::models::BaselineComparison>> {
380        let store = self.load()?;
381        Ok(store.compare_baselines(source_id, target_id))
382    }
383
384    // =========================================================================
385    // Utility Operations
386    // =========================================================================
387
388    /// Gets the current store version (for detecting external modifications)
389    ///
390    /// This is used for polling to detect if the database has been modified
391    /// by another process since we last loaded it.
392    fn get_store_version(&self) -> Result<i64> {
393        Ok(self.load()?.store_version)
394    }
395
396    /// Returns true if the database file exists
397    fn exists(&self) -> bool {
398        self.path().exists()
399    }
400
401    /// Creates the database with default/empty data if it doesn't exist
402    fn create_if_not_exists(&self) -> Result<()> {
403        if !self.exists() {
404            self.save(&RequirementsStore::new())?;
405        }
406        Ok(())
407    }
408
409    // =========================================================================
410    // Queue Operations (STORY-0366)
411    // =========================================================================
412
413    /// Lists queue entries for a user
414    /// If include_completed is false, excludes entries whose requirement is Completed
415    fn queue_list(&self, _user_id: &str, _include_completed: bool) -> Result<Vec<QueueEntry>> {
416        anyhow::bail!("Queue not supported for this backend")
417    }
418
419    /// Adds an entry to a user's queue
420    fn queue_add(&self, _entry: QueueEntry) -> Result<()> {
421        anyhow::bail!("Queue not supported for this backend")
422    }
423
424    /// Removes an entry from a user's queue
425    fn queue_remove(&self, _user_id: &str, _requirement_id: &Uuid) -> Result<()> {
426        anyhow::bail!("Queue not supported for this backend")
427    }
428
429    /// Reorders queue entries by updating positions
430    fn queue_reorder(&self, _user_id: &str, _items: &[(Uuid, i64)]) -> Result<()> {
431        anyhow::bail!("Queue not supported for this backend")
432    }
433
434    /// Clears queue entries. If completed_only is true, only removes entries
435    /// whose requirement has status Completed.
436    fn queue_clear(&self, _user_id: &str, _completed_only: bool) -> Result<()> {
437        anyhow::bail!("Queue not supported for this backend")
438    }
439
440    // =========================================================================
441    // Utility Operations
442    // =========================================================================
443
444    /// Returns statistics about the database
445    fn stats(&self) -> Result<DatabaseStats> {
446        let store = self.load()?;
447        Ok(DatabaseStats {
448            requirement_count: store.requirements.len(),
449            user_count: store.users.len(),
450            feature_count: store.features.len(),
451            backend_type: self.backend_type(),
452        })
453    }
454}
455
456/// Statistics about a database
457#[derive(Debug, Clone)]
458pub struct DatabaseStats {
459    pub requirement_count: usize,
460    pub user_count: usize,
461    pub feature_count: usize,
462    pub backend_type: BackendType,
463}