Skip to main content

aida_core/db/
sqlite_backend.rs

1//! SQLite database storage backend
2//!
3//! This backend stores requirements data in a SQLite database file,
4//! providing better concurrent access and query performance.
5
6use anyhow::{Context, Result};
7use rusqlite::{params, Connection, OptionalExtension};
8use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10use std::sync::Mutex;
11use uuid::Uuid;
12
13use crate::models::{
14    Attachment, Comment, CustomTypeDefinition, FeatureDefinition, GitLabIssueLink, HistoryEntry,
15    IdConfiguration, ImplementationInfo, QueueEntry, ReactionDefinition, Relationship,
16    RelationshipDefinition, Requirement, RequirementPriority, RequirementStatus, RequirementType,
17    RequirementsStore, TraceLink, UrlLink, User,
18};
19
20use super::traits::{BackendType, DatabaseBackend};
21
22/// Current schema version - updated to 8 for requirement weight/attachments/gitlab issues
23const SCHEMA_VERSION: i32 = 8;
24
25/// SQLite backend implementation
26pub struct SqliteBackend {
27    path: PathBuf,
28    conn: Mutex<Connection>,
29}
30
31impl SqliteBackend {
32    /// Creates a new SQLite backend
33    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
34        let path = path.as_ref().to_path_buf();
35
36        // Create parent directories if needed
37        if let Some(parent) = path.parent() {
38            std::fs::create_dir_all(parent)?;
39        }
40
41        let conn = Connection::open(&path)?;
42
43        // Enable WAL mode for better concurrent access
44        conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
45
46        let backend = Self {
47            path,
48            conn: Mutex::new(conn),
49        };
50
51        backend.init_schema()?;
52        Ok(backend)
53    }
54
55    /// Initialize the database schema
56    fn init_schema(&self) -> Result<()> {
57        let conn = self.conn.lock().unwrap();
58
59        // Check current schema version
60        let current_version: i32 = conn
61            .query_row("SELECT version FROM schema_version LIMIT 1", [], |row| {
62                row.get(0)
63            })
64            .unwrap_or(0);
65
66        if current_version == 0 {
67            // Create initial schema
68            conn.execute_batch(include_str!("schema.sql"))?;
69        } else if current_version < SCHEMA_VERSION {
70            // Handle migrations
71            Self::migrate_schema(&conn, current_version)?;
72        }
73
74        Ok(())
75    }
76
77    /// Migrate schema from old version to current
78    fn migrate_schema(conn: &Connection, from_version: i32) -> Result<()> {
79        if from_version < 2 {
80            // Migration from v1 to v2: Add version columns for optimistic locking
81            conn.execute_batch(
82                r#"
83                -- Add version column to requirements for optimistic locking
84                ALTER TABLE requirements ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
85
86                -- Add custom_priority column if missing
87                ALTER TABLE requirements ADD COLUMN custom_priority TEXT;
88
89                -- Add ai_evaluation column if missing
90                ALTER TABLE requirements ADD COLUMN ai_evaluation TEXT;
91
92                -- Add version column to users
93                ALTER TABLE users ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
94
95                -- Add new metadata columns
96                ALTER TABLE metadata ADD COLUMN ai_prompts TEXT NOT NULL DEFAULT '{}';
97                ALTER TABLE metadata ADD COLUMN baselines TEXT NOT NULL DEFAULT '[]';
98                ALTER TABLE metadata ADD COLUMN teams TEXT NOT NULL DEFAULT '[]';
99                ALTER TABLE metadata ADD COLUMN store_version INTEGER NOT NULL DEFAULT 1;
100
101                -- Update schema version
102                UPDATE schema_version SET version = 2;
103                "#,
104            )
105            .unwrap_or_else(|e| {
106                // Some columns may already exist, ignore those errors
107                eprintln!("Note: Some migration columns may already exist: {}", e);
108            });
109
110            // Ensure schema version is updated even if some ALTERs failed
111            let _ = conn.execute("UPDATE schema_version SET version = 2", []);
112        }
113
114        if from_version < 3 {
115            // Migration from v2 to v3: Add trace_links and implementation_info columns
116            // trace:REQ-0245 | ai:claude:high
117            conn.execute_batch(
118                r#"
119                -- Add trace_links column for code-to-requirement traceability
120                ALTER TABLE requirements ADD COLUMN trace_links TEXT NOT NULL DEFAULT '[]';
121
122                -- Add implementation_info column for implementation metadata
123                ALTER TABLE requirements ADD COLUMN implementation_info TEXT;
124
125                -- Update schema version
126                UPDATE schema_version SET version = 3;
127                "#,
128            )
129            .unwrap_or_else(|e| {
130                // Some columns may already exist, ignore those errors
131                eprintln!("Note: Some v3 migration columns may already exist: {}", e);
132            });
133
134            // Ensure schema version is updated even if some ALTERs failed
135            let _ = conn.execute("UPDATE schema_version SET version = 3", []);
136        }
137
138        if from_version < 4 {
139            // Migration from v3 to v4: Add pin_hash column to users table
140            // trace:AUTH-0001 | ai:claude:high
141            conn.execute_batch(
142                r#"
143                -- Add pin_hash column for simple user authentication
144                ALTER TABLE users ADD COLUMN pin_hash TEXT;
145
146                -- Update schema version
147                UPDATE schema_version SET version = 4;
148                "#,
149            )
150            .unwrap_or_else(|e| {
151                // Column may already exist, ignore those errors
152                eprintln!("Note: Some v4 migration columns may already exist: {}", e);
153            });
154
155            // Ensure schema version is updated even if ALTER failed
156            let _ = conn.execute("UPDATE schema_version SET version = 4", []);
157        }
158
159        if from_version < 5 {
160            // Migration from v4 to v5: Add gitlab_sync_state table
161            // trace:STORY-0325 | ai:claude
162            conn.execute_batch(
163                r#"
164                -- GitLab sync state table for tracking sync between AIDA and GitLab
165                CREATE TABLE IF NOT EXISTS gitlab_sync_state (
166                    requirement_id TEXT NOT NULL,
167                    spec_id TEXT NOT NULL,
168                    gitlab_project_id INTEGER NOT NULL,
169                    gitlab_issue_iid INTEGER NOT NULL,
170                    gitlab_issue_id INTEGER NOT NULL,
171                    linked_at TEXT NOT NULL,
172                    last_sync TEXT NOT NULL,
173                    aida_content_hash TEXT NOT NULL DEFAULT '',
174                    gitlab_content_hash TEXT NOT NULL DEFAULT '',
175                    link_origin TEXT NOT NULL DEFAULT 'ManualLink',
176                    sync_status TEXT NOT NULL DEFAULT 'Untracked',
177                    last_error TEXT,
178                    PRIMARY KEY (requirement_id, gitlab_issue_iid)
179                );
180
181                -- Index for looking up sync state by requirement
182                CREATE INDEX IF NOT EXISTS idx_gitlab_sync_requirement ON gitlab_sync_state(requirement_id);
183
184                -- Index for looking up sync state by GitLab issue
185                CREATE INDEX IF NOT EXISTS idx_gitlab_sync_issue ON gitlab_sync_state(gitlab_project_id, gitlab_issue_iid);
186
187                -- Index for filtering by sync status
188                CREATE INDEX IF NOT EXISTS idx_gitlab_sync_status ON gitlab_sync_state(sync_status);
189
190                -- Update schema version
191                UPDATE schema_version SET version = 5;
192                "#,
193            ).unwrap_or_else(|e| {
194                eprintln!("Note: Some v5 migration may already exist: {}", e);
195            });
196
197            // Ensure schema version is updated
198            let _ = conn.execute("UPDATE schema_version SET version = 5", []);
199        }
200
201        // Migrate from version 5 to version 6 (add meta_subtype column)
202        if from_version < 6 {
203            conn.execute_batch(
204                r#"
205                -- Add meta_subtype column for Meta requirements
206                ALTER TABLE requirements ADD COLUMN meta_subtype TEXT;
207
208                -- Update schema version
209                UPDATE schema_version SET version = 6;
210                "#,
211            )
212            .unwrap_or_else(|e| {
213                eprintln!("Note: Some v6 migration may already exist: {}", e);
214            });
215
216            // Ensure schema version is updated
217            let _ = conn.execute("UPDATE schema_version SET version = 6", []);
218        }
219
220        // Migrate from version 6 to version 7 (add queue_entries table)
221        // trace:STORY-0366 | ai:claude
222        if from_version < 7 {
223            conn.execute_batch(
224                r#"
225                -- Queue entries table for personal work queue per user
226                CREATE TABLE IF NOT EXISTS queue_entries (
227                    user_id TEXT NOT NULL,
228                    requirement_id TEXT NOT NULL,
229                    position INTEGER NOT NULL,
230                    added_by TEXT NOT NULL,
231                    note TEXT,
232                    added_at TEXT NOT NULL,
233                    PRIMARY KEY (user_id, requirement_id)
234                );
235
236                -- Index for efficient queue listing ordered by position
237                CREATE INDEX IF NOT EXISTS idx_queue_user_position ON queue_entries(user_id, position);
238
239                -- Update schema version
240                UPDATE schema_version SET version = 7;
241                "#,
242            ).unwrap_or_else(|e| {
243                eprintln!("Note: Some v7 migration may already exist: {}", e);
244            });
245
246            // Ensure schema version is updated
247            let _ = conn.execute("UPDATE schema_version SET version = 7", []);
248        }
249
250        // Migrate from version 7 to version 8 (persist additional requirement fields)
251        if from_version < 8 {
252            conn.execute_batch(
253                r#"
254                ALTER TABLE requirements ADD COLUMN weight REAL;
255                ALTER TABLE requirements ADD COLUMN attachments TEXT NOT NULL DEFAULT '[]';
256                ALTER TABLE requirements ADD COLUMN gitlab_issues TEXT NOT NULL DEFAULT '[]';
257
258                UPDATE schema_version SET version = 8;
259                "#,
260            )
261            .unwrap_or_else(|e| {
262                eprintln!("Note: Some v8 migration columns may already exist: {}", e);
263            });
264
265            let _ = conn.execute("UPDATE schema_version SET version = 8", []);
266        }
267
268        Ok(())
269    }
270
271    /// Serializes complex types to JSON for storage
272    fn to_json<T: serde::Serialize>(value: &T) -> Result<String> {
273        serde_json::to_string(value).context("Failed to serialize to JSON")
274    }
275
276    /// Deserializes complex types from JSON storage
277    fn from_json<T: serde::de::DeserializeOwned>(json: &str) -> Result<T> {
278        serde_json::from_str(json).context("Failed to deserialize from JSON")
279    }
280
281    /// Converts a RequirementStatus to a string for storage
282    fn status_to_str(status: &RequirementStatus) -> &'static str {
283        match status {
284            RequirementStatus::Draft => "Draft",
285            RequirementStatus::Approved => "Approved",
286            RequirementStatus::Planned => "Planned",
287            RequirementStatus::InProgress => "In Progress",
288            RequirementStatus::Completed => "Completed",
289            RequirementStatus::Rejected => "Rejected",
290        }
291    }
292
293    /// Parses a RequirementStatus from a string
294    fn str_to_status(s: &str) -> RequirementStatus {
295        match s {
296            "Draft" => RequirementStatus::Draft,
297            "Approved" => RequirementStatus::Approved,
298            "Planned" => RequirementStatus::Planned,
299            "In Progress" => RequirementStatus::InProgress,
300            "Completed" => RequirementStatus::Completed,
301            "Rejected" => RequirementStatus::Rejected,
302            _ => RequirementStatus::Draft,
303        }
304    }
305
306    /// Converts a RequirementPriority to a string for storage
307    fn priority_to_str(priority: &RequirementPriority) -> &'static str {
308        match priority {
309            RequirementPriority::High => "High",
310            RequirementPriority::Medium => "Medium",
311            RequirementPriority::Low => "Low",
312        }
313    }
314
315    /// Parses a RequirementPriority from a string
316    fn str_to_priority(s: &str) -> RequirementPriority {
317        match s {
318            "High" => RequirementPriority::High,
319            "Medium" => RequirementPriority::Medium,
320            "Low" => RequirementPriority::Low,
321            _ => RequirementPriority::Medium,
322        }
323    }
324
325    /// Converts a RequirementType to a string for storage
326    fn type_to_str(req_type: &RequirementType) -> &'static str {
327        match req_type {
328            RequirementType::Functional => "Functional",
329            RequirementType::NonFunctional => "NonFunctional",
330            RequirementType::System => "System",
331            RequirementType::User => "User",
332            RequirementType::ChangeRequest => "ChangeRequest",
333            RequirementType::Bug => "Bug",
334            RequirementType::Epic => "Epic",
335            RequirementType::Story => "Story",
336            RequirementType::Task => "Task",
337            RequirementType::Spike => "Spike",
338            RequirementType::Sprint => "Sprint",
339            RequirementType::Folder => "Folder",
340            RequirementType::Meta => "Meta",
341        }
342    }
343
344    /// Parses a RequirementType from a string
345    fn str_to_type(s: &str) -> RequirementType {
346        match s {
347            "Functional" => RequirementType::Functional,
348            "NonFunctional" => RequirementType::NonFunctional,
349            "System" => RequirementType::System,
350            "User" => RequirementType::User,
351            "ChangeRequest" => RequirementType::ChangeRequest,
352            "Bug" => RequirementType::Bug,
353            "Epic" => RequirementType::Epic,
354            "Story" => RequirementType::Story,
355            "Task" => RequirementType::Task,
356            "Spike" => RequirementType::Spike,
357            "Sprint" => RequirementType::Sprint,
358            "Folder" => RequirementType::Folder,
359            "Meta" => RequirementType::Meta,
360            _ => RequirementType::Functional,
361        }
362    }
363
364    /// Load requirements from database
365    fn load_requirements(&self, conn: &Connection) -> Result<Vec<Requirement>> {
366        // Check if new columns exist (for schema migration compatibility)
367        let has_trace_links = conn
368            .query_row(
369                "SELECT COUNT(*) FROM pragma_table_info('requirements') WHERE name='trace_links'",
370                [],
371                |row| row.get::<_, i32>(0),
372            )
373            .unwrap_or(0)
374            > 0;
375
376        let query = if has_trace_links {
377            "SELECT id, spec_id, prefix_override, title, description, status, priority,
378                    owner, feature, created_at, created_by, modified_at, req_type,
379                    dependencies, tags, relationships, comments, history, archived,
380                    custom_status, custom_priority, custom_fields, urls, trace_links,
381                    implementation_info, ai_evaluation, weight, attachments, gitlab_issues, version
382             FROM requirements ORDER BY created_at"
383        } else {
384            "SELECT id, spec_id, prefix_override, title, description, status, priority,
385                    owner, feature, created_at, created_by, modified_at, req_type,
386                    dependencies, tags, relationships, comments, history, archived,
387                    custom_status, custom_priority, custom_fields, urls, NULL as trace_links,
388                    NULL as implementation_info, ai_evaluation, NULL as weight, '[]' as attachments, '[]' as gitlab_issues, version
389             FROM requirements ORDER BY created_at"
390        };
391
392        let mut stmt = conn.prepare(query)?;
393
394        let rows = stmt.query_map([], |row| {
395            let id_str: String = row.get(0)?;
396            let spec_id: Option<String> = row.get(1)?;
397            let prefix_override: Option<String> = row.get(2)?;
398            let title: String = row.get(3)?;
399            let description: String = row.get(4)?;
400            let status_str: String = row.get(5)?;
401            let priority_str: String = row.get(6)?;
402            let owner: String = row.get(7)?;
403            let feature: String = row.get(8)?;
404            let created_at_str: String = row.get(9)?;
405            let created_by: Option<String> = row.get(10)?;
406            let modified_at_str: String = row.get(11)?;
407            let req_type_str: String = row.get(12)?;
408            let dependencies_json: String = row.get(13)?;
409            let tags_json: String = row.get(14)?;
410            let relationships_json: String = row.get(15)?;
411            let comments_json: String = row.get(16)?;
412            let history_json: String = row.get(17)?;
413            let archived: bool = row.get(18)?;
414            let custom_status: Option<String> = row.get(19)?;
415            let custom_priority: Option<String> = row.get(20)?;
416            let custom_fields_json: String = row.get(21)?;
417            let urls_json: String = row.get(22)?;
418            let trace_links_json: Option<String> = row.get(23)?;
419            let implementation_info_json: Option<String> = row.get(24)?;
420            let ai_evaluation_json: Option<String> = row.get(25)?;
421            let weight: Option<f32> = row.get(26)?;
422            let attachments_json: String = row.get(27)?;
423            let gitlab_issues_json: String = row.get(28)?;
424            let version: i64 = row.get(29)?;
425
426            Ok((
427                id_str,
428                spec_id,
429                prefix_override,
430                title,
431                description,
432                status_str,
433                priority_str,
434                owner,
435                feature,
436                created_at_str,
437                created_by,
438                modified_at_str,
439                req_type_str,
440                dependencies_json,
441                tags_json,
442                relationships_json,
443                comments_json,
444                history_json,
445                archived,
446                custom_status,
447                custom_priority,
448                custom_fields_json,
449                urls_json,
450                trace_links_json,
451                implementation_info_json,
452                ai_evaluation_json,
453                weight,
454                attachments_json,
455                gitlab_issues_json,
456                version,
457            ))
458        })?;
459
460        let mut requirements = Vec::new();
461        for row_result in rows {
462            let (
463                id_str,
464                spec_id,
465                prefix_override,
466                title,
467                description,
468                status_str,
469                priority_str,
470                owner,
471                feature,
472                created_at_str,
473                created_by,
474                modified_at_str,
475                req_type_str,
476                dependencies_json,
477                tags_json,
478                relationships_json,
479                comments_json,
480                history_json,
481                archived,
482                custom_status,
483                custom_priority,
484                custom_fields_json,
485                urls_json,
486                trace_links_json,
487                implementation_info_json,
488                ai_evaluation_json,
489                weight,
490                attachments_json,
491                gitlab_issues_json,
492                version,
493            ) = row_result?;
494
495            let id = Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::now_v7());
496            let status = Self::str_to_status(&status_str);
497            let priority = Self::str_to_priority(&priority_str);
498            let req_type = Self::str_to_type(&req_type_str);
499            let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
500                .map(|dt| dt.with_timezone(&chrono::Utc))
501                .unwrap_or_else(|_| chrono::Utc::now());
502            let modified_at = chrono::DateTime::parse_from_rfc3339(&modified_at_str)
503                .map(|dt| dt.with_timezone(&chrono::Utc))
504                .unwrap_or_else(|_| chrono::Utc::now());
505            let dependencies: Vec<Uuid> = Self::from_json(&dependencies_json).unwrap_or_default();
506            let tags: HashSet<String> = Self::from_json(&tags_json).unwrap_or_default();
507            let relationships: Vec<Relationship> =
508                Self::from_json(&relationships_json).unwrap_or_default();
509            let comments: Vec<Comment> = Self::from_json(&comments_json).unwrap_or_default();
510            let history: Vec<HistoryEntry> = Self::from_json(&history_json).unwrap_or_default();
511            let custom_fields: HashMap<String, String> =
512                Self::from_json(&custom_fields_json).unwrap_or_default();
513            let urls: Vec<UrlLink> = Self::from_json(&urls_json).unwrap_or_default();
514            let trace_links: Vec<TraceLink> = trace_links_json
515                .and_then(|json| Self::from_json(&json).ok())
516                .unwrap_or_default();
517            let implementation_info: Option<ImplementationInfo> =
518                implementation_info_json.and_then(|json| Self::from_json(&json).ok());
519            let ai_evaluation = ai_evaluation_json.and_then(|json| Self::from_json(&json).ok());
520            let attachments: Vec<Attachment> =
521                Self::from_json(&attachments_json).unwrap_or_default();
522            let gitlab_issues: Vec<GitLabIssueLink> =
523                Self::from_json(&gitlab_issues_json).unwrap_or_default();
524
525            requirements.push(Requirement {
526                id,
527                spec_id,
528                agreed_id: None,
529                prefix_override,
530                title,
531                description,
532                status,
533                priority,
534                owner,
535                feature,
536                created_at,
537                created_by,
538                modified_at,
539                req_type,
540                meta_subtype: None, // Loaded separately if needed
541                dependencies,
542                tags,
543                weight,
544                relationships,
545                comments,
546                history,
547                archived,
548                custom_status,
549                custom_priority,
550                custom_fields,
551                urls,
552                attachments,
553                trace_links,
554                gitlab_issues,
555                implementation_info,
556                ai_evaluation,
557                version,
558            });
559        }
560
561        Ok(requirements)
562    }
563
564    /// Load users from database
565    fn load_users(&self, conn: &Connection) -> Result<Vec<User>> {
566        let mut stmt = conn.prepare(
567            "SELECT id, spec_id, name, email, handle, pin_hash, created_at, archived, version FROM users"
568        )?;
569
570        let rows = stmt.query_map([], |row| {
571            let id_str: String = row.get(0)?;
572            let spec_id: Option<String> = row.get(1)?;
573            let name: String = row.get(2)?;
574            let email: String = row.get(3)?;
575            let handle: String = row.get(4)?;
576            let pin_hash: Option<String> = row.get(5)?;
577            let created_at_str: String = row.get(6)?;
578            let archived: bool = row.get(7)?;
579            let version: i64 = row.get(8)?;
580            Ok((
581                id_str,
582                spec_id,
583                name,
584                email,
585                handle,
586                pin_hash,
587                created_at_str,
588                archived,
589                version,
590            ))
591        })?;
592
593        let mut users = Vec::new();
594        for row_result in rows {
595            let (id_str, spec_id, name, email, handle, pin_hash, created_at_str, archived, version): (String, Option<String>, String, String, String, Option<String>, String, bool, i64) = row_result?;
596            let id = Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::now_v7());
597            let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
598                .map(|dt| dt.with_timezone(&chrono::Utc))
599                .unwrap_or_else(|_| chrono::Utc::now());
600
601            users.push(User {
602                id,
603                spec_id,
604                name,
605                email,
606                handle,
607                pin_hash,
608                created_at,
609                archived,
610                version,
611            });
612        }
613
614        Ok(users)
615    }
616
617    /// Load metadata from database
618    fn load_metadata(
619        &self,
620        conn: &Connection,
621    ) -> Result<(
622        String,
623        String,
624        String,
625        IdConfiguration,
626        u32,
627        u32,
628        HashMap<String, u32>,
629        HashMap<String, u32>,
630    )> {
631        let row = conn.query_row(
632            "SELECT name, title, description, id_config, next_feature_number, next_spec_number, prefix_counters, meta_counters
633             FROM metadata WHERE id = 1",
634            [],
635            |row| {
636                let name: String = row.get(0)?;
637                let title: String = row.get(1)?;
638                let description: String = row.get(2)?;
639                let id_config_json: String = row.get(3)?;
640                let next_feature_number: u32 = row.get(4)?;
641                let next_spec_number: u32 = row.get(5)?;
642                let prefix_counters_json: String = row.get(6)?;
643                let meta_counters_json: String = row.get(7)?;
644                Ok((name, title, description, id_config_json, next_feature_number, next_spec_number, prefix_counters_json, meta_counters_json))
645            }
646        ).optional()?;
647
648        match row {
649            Some((
650                name,
651                title,
652                description,
653                id_config_json,
654                next_feature_number,
655                next_spec_number,
656                prefix_counters_json,
657                meta_counters_json,
658            )) => {
659                let id_config: IdConfiguration =
660                    Self::from_json(&id_config_json).unwrap_or_default();
661                let prefix_counters: HashMap<String, u32> =
662                    Self::from_json(&prefix_counters_json).unwrap_or_default();
663                let meta_counters: HashMap<String, u32> =
664                    Self::from_json(&meta_counters_json).unwrap_or_default();
665                Ok((
666                    name,
667                    title,
668                    description,
669                    id_config,
670                    next_feature_number,
671                    next_spec_number,
672                    prefix_counters,
673                    meta_counters,
674                ))
675            }
676            None => Ok((
677                String::new(),
678                String::new(),
679                String::new(),
680                IdConfiguration::default(),
681                1,
682                1,
683                HashMap::new(),
684                HashMap::new(),
685            )),
686        }
687    }
688
689    /// Load features from database
690    fn load_features(&self, conn: &Connection) -> Result<Vec<FeatureDefinition>> {
691        let json: String = conn
692            .query_row("SELECT features FROM metadata WHERE id = 1", [], |row| {
693                row.get(0)
694            })
695            .unwrap_or_else(|_| "[]".to_string());
696        Self::from_json(&json)
697    }
698
699    /// Load type definitions from database
700    fn load_type_definitions(&self, conn: &Connection) -> Result<Vec<CustomTypeDefinition>> {
701        let json: String = conn
702            .query_row(
703                "SELECT type_definitions FROM metadata WHERE id = 1",
704                [],
705                |row| row.get(0),
706            )
707            .unwrap_or_else(|_| "[]".to_string());
708        let defs: Vec<CustomTypeDefinition> = Self::from_json(&json)?;
709        if defs.is_empty() {
710            Ok(crate::models::default_type_definitions())
711        } else {
712            Ok(defs)
713        }
714    }
715
716    /// Load relationship definitions from database
717    fn load_relationship_definitions(
718        &self,
719        conn: &Connection,
720    ) -> Result<Vec<RelationshipDefinition>> {
721        let json: String = conn
722            .query_row(
723                "SELECT relationship_definitions FROM metadata WHERE id = 1",
724                [],
725                |row| row.get(0),
726            )
727            .unwrap_or_else(|_| "[]".to_string());
728        let defs: Vec<RelationshipDefinition> = Self::from_json(&json)?;
729        if defs.is_empty() {
730            Ok(RelationshipDefinition::defaults())
731        } else {
732            Ok(defs)
733        }
734    }
735
736    /// Load reaction definitions from database
737    fn load_reaction_definitions(&self, conn: &Connection) -> Result<Vec<ReactionDefinition>> {
738        let json: String = conn
739            .query_row(
740                "SELECT reaction_definitions FROM metadata WHERE id = 1",
741                [],
742                |row| row.get(0),
743            )
744            .unwrap_or_else(|_| "[]".to_string());
745        let defs: Vec<ReactionDefinition> = Self::from_json(&json)?;
746        if defs.is_empty() {
747            Ok(crate::models::default_reaction_definitions())
748        } else {
749            Ok(defs)
750        }
751    }
752
753    /// Load allowed prefixes from database
754    fn load_allowed_prefixes(&self, conn: &Connection) -> Result<(Vec<String>, bool)> {
755        let row = conn
756            .query_row(
757                "SELECT allowed_prefixes, restrict_prefixes FROM metadata WHERE id = 1",
758                [],
759                |row| {
760                    let json: String = row.get(0)?;
761                    let restrict: bool = row.get(1)?;
762                    Ok((json, restrict))
763                },
764            )
765            .optional()?;
766
767        match row {
768            Some((json, restrict)) => {
769                let prefixes: Vec<String> = Self::from_json(&json).unwrap_or_default();
770                Ok((prefixes, restrict))
771            }
772            None => Ok((Vec::new(), false)),
773        }
774    }
775
776    /// Save a requirement to the database (for full store save)
777    fn save_requirement(&self, conn: &Connection, req: &Requirement) -> Result<()> {
778        let ai_eval_json = req
779            .ai_evaluation
780            .as_ref()
781            .map(|e| Self::to_json(e))
782            .transpose()?;
783        let impl_info_json = req
784            .implementation_info
785            .as_ref()
786            .map(|i| Self::to_json(i))
787            .transpose()?;
788        let attachments_json = Self::to_json(&req.attachments)?;
789        let gitlab_issues_json = Self::to_json(&req.gitlab_issues)?;
790
791        // Check if new columns exist and use appropriate query
792        let has_trace_links = conn
793            .query_row(
794                "SELECT COUNT(*) FROM pragma_table_info('requirements') WHERE name='trace_links'",
795                [],
796                |row| row.get::<_, i32>(0),
797            )
798            .unwrap_or(0)
799            > 0;
800
801        if has_trace_links {
802            conn.execute(
803                "INSERT OR REPLACE INTO requirements
804                 (id, spec_id, prefix_override, title, description, status, priority, owner, feature,
805                  created_at, created_by, modified_at, req_type, dependencies, tags, relationships,
806                  comments, history, archived, custom_status, custom_priority, custom_fields, urls,
807                  trace_links, implementation_info, ai_evaluation, weight, attachments, gitlab_issues, version)
808                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30)",
809                params![
810                    req.id.to_string(),
811                    req.spec_id,
812                    req.prefix_override,
813                    req.title,
814                    req.description,
815                    Self::status_to_str(&req.status),
816                    Self::priority_to_str(&req.priority),
817                    req.owner,
818                    req.feature,
819                    req.created_at.to_rfc3339(),
820                    req.created_by,
821                    req.modified_at.to_rfc3339(),
822                    Self::type_to_str(&req.req_type),
823                    Self::to_json(&req.dependencies)?,
824                    Self::to_json(&req.tags)?,
825                    Self::to_json(&req.relationships)?,
826                    Self::to_json(&req.comments)?,
827                    Self::to_json(&req.history)?,
828                    req.archived,
829                    req.custom_status,
830                    req.custom_priority,
831                    Self::to_json(&req.custom_fields)?,
832                    Self::to_json(&req.urls)?,
833                    Self::to_json(&req.trace_links)?,
834                    impl_info_json,
835                    ai_eval_json,
836                    req.weight,
837                    attachments_json,
838                    gitlab_issues_json,
839                    req.version,
840                ],
841            )?;
842        } else {
843            // Fallback for old schema without trace_links
844            conn.execute(
845                "INSERT OR REPLACE INTO requirements
846                 (id, spec_id, prefix_override, title, description, status, priority, owner, feature,
847                  created_at, created_by, modified_at, req_type, dependencies, tags, relationships,
848                  comments, history, archived, custom_status, custom_priority, custom_fields, urls,
849                  ai_evaluation, version)
850                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25)",
851                params![
852                    req.id.to_string(),
853                    req.spec_id,
854                    req.prefix_override,
855                    req.title,
856                    req.description,
857                    Self::status_to_str(&req.status),
858                    Self::priority_to_str(&req.priority),
859                    req.owner,
860                    req.feature,
861                    req.created_at.to_rfc3339(),
862                    req.created_by,
863                    req.modified_at.to_rfc3339(),
864                    Self::type_to_str(&req.req_type),
865                    Self::to_json(&req.dependencies)?,
866                    Self::to_json(&req.tags)?,
867                    Self::to_json(&req.relationships)?,
868                    Self::to_json(&req.comments)?,
869                    Self::to_json(&req.history)?,
870                    req.archived,
871                    req.custom_status,
872                    req.custom_priority,
873                    Self::to_json(&req.custom_fields)?,
874                    Self::to_json(&req.urls)?,
875                    ai_eval_json,
876                    req.version,
877                ],
878            )?;
879        }
880        Ok(())
881    }
882
883    /// Save a user to the database
884    fn save_user(&self, conn: &Connection, user: &User) -> Result<()> {
885        conn.execute(
886            "INSERT OR REPLACE INTO users (id, spec_id, name, email, handle, pin_hash, created_at, archived, version)
887             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
888            params![
889                user.id.to_string(),
890                user.spec_id,
891                user.name,
892                user.email,
893                user.handle,
894                user.pin_hash,
895                user.created_at.to_rfc3339(),
896                user.archived,
897                user.version,
898            ],
899        )?;
900        Ok(())
901    }
902
903    /// Save metadata to the database
904    fn save_metadata(&self, conn: &Connection, store: &RequirementsStore) -> Result<()> {
905        conn.execute(
906            "INSERT OR REPLACE INTO metadata
907             (id, name, title, description, id_config, features, next_feature_number, next_spec_number,
908              prefix_counters, relationship_definitions, reaction_definitions, meta_counters,
909              type_definitions, allowed_prefixes, restrict_prefixes, ai_prompts, baselines, teams, store_version)
910             VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
911            params![
912                store.name,
913                store.title,
914                store.description,
915                Self::to_json(&store.id_config)?,
916                Self::to_json(&store.features)?,
917                store.next_feature_number,
918                store.next_spec_number,
919                Self::to_json(&store.prefix_counters)?,
920                Self::to_json(&store.relationship_definitions)?,
921                Self::to_json(&store.reaction_definitions)?,
922                Self::to_json(&store.meta_counters)?,
923                Self::to_json(&store.type_definitions)?,
924                Self::to_json(&store.allowed_prefixes)?,
925                store.restrict_prefixes,
926                Self::to_json(&store.ai_prompts)?,
927                Self::to_json(&store.baselines)?,
928                Self::to_json(&store.teams)?,
929                store.store_version,
930            ],
931        )?;
932        Ok(())
933    }
934
935    /// Load store_version from metadata
936    fn load_store_version(&self, conn: &Connection) -> Result<i64> {
937        let version: i64 = conn
938            .query_row(
939                "SELECT store_version FROM metadata WHERE id = 1",
940                [],
941                |row| row.get(0),
942            )
943            .unwrap_or(1);
944        Ok(version)
945    }
946
947    /// Load ai_prompts from metadata
948    fn load_ai_prompts(&self, conn: &Connection) -> Result<crate::models::AiPromptConfig> {
949        let json: String = conn
950            .query_row("SELECT ai_prompts FROM metadata WHERE id = 1", [], |row| {
951                row.get(0)
952            })
953            .unwrap_or_else(|_| "{}".to_string());
954        Self::from_json(&json).or_else(|_| Ok(crate::models::AiPromptConfig::default()))
955    }
956
957    /// Load baselines from metadata
958    fn load_baselines(&self, conn: &Connection) -> Result<Vec<crate::models::Baseline>> {
959        let json: String = conn
960            .query_row("SELECT baselines FROM metadata WHERE id = 1", [], |row| {
961                row.get(0)
962            })
963            .unwrap_or_else(|_| "[]".to_string());
964        Self::from_json(&json)
965    }
966
967    /// Load teams from metadata
968    fn load_teams(&self, conn: &Connection) -> Result<Vec<crate::models::Team>> {
969        let json: String = conn
970            .query_row("SELECT teams FROM metadata WHERE id = 1", [], |row| {
971                row.get(0)
972            })
973            .unwrap_or_else(|_| "[]".to_string());
974        Self::from_json(&json)
975    }
976
977    // ==================== GitLab Sync State Operations (STORY-0325) ====================
978
979    /// Save or update a GitLab sync state
980    /// trace:STORY-0325 | ai:claude
981    pub fn save_sync_state(&self, state: &crate::models::GitLabSyncState) -> Result<()> {
982        let conn = self.conn.lock().unwrap();
983        conn.execute(
984            r#"INSERT OR REPLACE INTO gitlab_sync_state
985               (requirement_id, spec_id, gitlab_project_id, gitlab_issue_iid, gitlab_issue_id,
986                linked_at, last_sync, aida_content_hash, gitlab_content_hash,
987                link_origin, sync_status, last_error)
988               VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"#,
989            rusqlite::params![
990                state.requirement_id.to_string(),
991                state.spec_id,
992                state.gitlab_project_id as i64,
993                state.gitlab_issue_iid as i64,
994                state.gitlab_issue_id as i64,
995                state.linked_at.to_rfc3339(),
996                state.last_sync.to_rfc3339(),
997                state.aida_content_hash,
998                state.gitlab_content_hash,
999                format!("{:?}", state.link_origin),
1000                format!("{:?}", state.sync_status),
1001                state.last_error,
1002            ],
1003        )?;
1004        Ok(())
1005    }
1006
1007    /// Load sync state for a specific requirement and issue
1008    /// trace:STORY-0325 | ai:claude
1009    pub fn load_sync_state(
1010        &self,
1011        requirement_id: Uuid,
1012        issue_iid: u64,
1013    ) -> Result<Option<crate::models::GitLabSyncState>> {
1014        let conn = self.conn.lock().unwrap();
1015        let result = conn.query_row(
1016            r#"SELECT requirement_id, spec_id, gitlab_project_id, gitlab_issue_iid, gitlab_issue_id,
1017                      linked_at, last_sync, aida_content_hash, gitlab_content_hash,
1018                      link_origin, sync_status, last_error
1019               FROM gitlab_sync_state WHERE requirement_id = ?1 AND gitlab_issue_iid = ?2"#,
1020            rusqlite::params![requirement_id.to_string(), issue_iid as i64],
1021            |row| Self::row_to_sync_state(row),
1022        );
1023
1024        match result {
1025            Ok(state) => Ok(Some(state)),
1026            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
1027            Err(e) => Err(e.into()),
1028        }
1029    }
1030
1031    /// Load all sync states for a requirement
1032    /// trace:STORY-0325 | ai:claude
1033    pub fn load_sync_states_for_requirement(
1034        &self,
1035        requirement_id: Uuid,
1036    ) -> Result<Vec<crate::models::GitLabSyncState>> {
1037        let conn = self.conn.lock().unwrap();
1038        let mut stmt = conn.prepare(
1039            r#"SELECT requirement_id, spec_id, gitlab_project_id, gitlab_issue_iid, gitlab_issue_id,
1040                      linked_at, last_sync, aida_content_hash, gitlab_content_hash,
1041                      link_origin, sync_status, last_error
1042               FROM gitlab_sync_state WHERE requirement_id = ?1"#,
1043        )?;
1044
1045        let states = stmt
1046            .query_map([requirement_id.to_string()], |row| {
1047                Self::row_to_sync_state(row)
1048            })?
1049            .filter_map(|r| r.ok())
1050            .collect();
1051
1052        Ok(states)
1053    }
1054
1055    /// Load all sync states
1056    /// trace:STORY-0325 | ai:claude
1057    pub fn load_all_sync_states(&self) -> Result<Vec<crate::models::GitLabSyncState>> {
1058        let conn = self.conn.lock().unwrap();
1059        let mut stmt = conn.prepare(
1060            r#"SELECT requirement_id, spec_id, gitlab_project_id, gitlab_issue_iid, gitlab_issue_id,
1061                      linked_at, last_sync, aida_content_hash, gitlab_content_hash,
1062                      link_origin, sync_status, last_error
1063               FROM gitlab_sync_state ORDER BY last_sync DESC"#,
1064        )?;
1065
1066        let states = stmt
1067            .query_map([], |row| Self::row_to_sync_state(row))?
1068            .filter_map(|r| r.ok())
1069            .collect();
1070
1071        Ok(states)
1072    }
1073
1074    /// Load sync states by status (e.g., all diverged items)
1075    /// trace:STORY-0325 | ai:claude
1076    pub fn load_sync_states_by_status(
1077        &self,
1078        status: crate::models::SyncStatus,
1079    ) -> Result<Vec<crate::models::GitLabSyncState>> {
1080        let conn = self.conn.lock().unwrap();
1081        let mut stmt = conn.prepare(
1082            r#"SELECT requirement_id, spec_id, gitlab_project_id, gitlab_issue_iid, gitlab_issue_id,
1083                      linked_at, last_sync, aida_content_hash, gitlab_content_hash,
1084                      link_origin, sync_status, last_error
1085               FROM gitlab_sync_state WHERE sync_status = ?1 ORDER BY last_sync DESC"#,
1086        )?;
1087
1088        let states = stmt
1089            .query_map([format!("{:?}", status)], |row| {
1090                Self::row_to_sync_state(row)
1091            })?
1092            .filter_map(|r| r.ok())
1093            .collect();
1094
1095        Ok(states)
1096    }
1097
1098    /// Delete a sync state
1099    /// trace:STORY-0325 | ai:claude
1100    pub fn delete_sync_state(&self, requirement_id: Uuid, issue_iid: u64) -> Result<bool> {
1101        let conn = self.conn.lock().unwrap();
1102        let rows_affected = conn.execute(
1103            "DELETE FROM gitlab_sync_state WHERE requirement_id = ?1 AND gitlab_issue_iid = ?2",
1104            rusqlite::params![requirement_id.to_string(), issue_iid as i64],
1105        )?;
1106        Ok(rows_affected > 0)
1107    }
1108
1109    /// Helper to convert a row to GitLabSyncState
1110    fn row_to_sync_state(row: &rusqlite::Row) -> rusqlite::Result<crate::models::GitLabSyncState> {
1111        use crate::models::{GitLabSyncState, LinkOrigin, SyncStatus};
1112
1113        let req_id_str: String = row.get(0)?;
1114        let linked_at_str: String = row.get(5)?;
1115        let last_sync_str: String = row.get(6)?;
1116        let link_origin_str: String = row.get(9)?;
1117        let sync_status_str: String = row.get(10)?;
1118
1119        Ok(GitLabSyncState {
1120            requirement_id: Uuid::parse_str(&req_id_str).unwrap_or_default(),
1121            spec_id: row.get(1)?,
1122            gitlab_project_id: row.get::<_, i64>(2)? as u64,
1123            gitlab_issue_iid: row.get::<_, i64>(3)? as u64,
1124            gitlab_issue_id: row.get::<_, i64>(4)? as u64,
1125            linked_at: chrono::DateTime::parse_from_rfc3339(&linked_at_str)
1126                .map(|dt| dt.with_timezone(&chrono::Utc))
1127                .unwrap_or_else(|_| chrono::Utc::now()),
1128            last_sync: chrono::DateTime::parse_from_rfc3339(&last_sync_str)
1129                .map(|dt| dt.with_timezone(&chrono::Utc))
1130                .unwrap_or_else(|_| chrono::Utc::now()),
1131            aida_content_hash: row.get(7)?,
1132            gitlab_content_hash: row.get(8)?,
1133            link_origin: match link_origin_str.as_str() {
1134                "CreatedFromAida" => LinkOrigin::CreatedFromAida,
1135                "ImportedFromGitLab" => LinkOrigin::ImportedFromGitLab,
1136                _ => LinkOrigin::ManualLink,
1137            },
1138            sync_status: match sync_status_str.as_str() {
1139                "InSync" => SyncStatus::InSync,
1140                "AidaModified" => SyncStatus::AidaModified,
1141                "GitLabModified" => SyncStatus::GitLabModified,
1142                "Conflict" => SyncStatus::Conflict,
1143                "Error" => SyncStatus::Error,
1144                _ => SyncStatus::Untracked,
1145            },
1146            last_error: row.get(11)?,
1147        })
1148    }
1149}
1150
1151impl DatabaseBackend for SqliteBackend {
1152    fn backend_type(&self) -> BackendType {
1153        BackendType::Sqlite
1154    }
1155
1156    fn path(&self) -> &Path {
1157        &self.path
1158    }
1159
1160    fn load(&self) -> Result<RequirementsStore> {
1161        let conn = self.conn.lock().unwrap();
1162
1163        let requirements = self.load_requirements(&conn)?;
1164        let users = self.load_users(&conn)?;
1165        let (
1166            name,
1167            title,
1168            description,
1169            id_config,
1170            next_feature_number,
1171            next_spec_number,
1172            prefix_counters,
1173            meta_counters,
1174        ) = self.load_metadata(&conn)?;
1175        let features = self.load_features(&conn)?;
1176        let type_definitions = self.load_type_definitions(&conn)?;
1177        let relationship_definitions = self.load_relationship_definitions(&conn)?;
1178        let reaction_definitions = self.load_reaction_definitions(&conn)?;
1179        let (allowed_prefixes, restrict_prefixes) = self.load_allowed_prefixes(&conn)?;
1180        let ai_prompts = self.load_ai_prompts(&conn)?;
1181        let baselines = self.load_baselines(&conn)?;
1182        let teams = self.load_teams(&conn)?;
1183        let store_version = self.load_store_version(&conn)?;
1184
1185        Ok(RequirementsStore {
1186            name,
1187            title,
1188            description,
1189            requirements,
1190            users,
1191            teams,
1192            id_config,
1193            features,
1194            next_feature_number,
1195            next_spec_number,
1196            prefix_counters,
1197            relationship_definitions,
1198            reaction_definitions,
1199            meta_counters,
1200            type_definitions,
1201            allowed_prefixes,
1202            restrict_prefixes,
1203            ai_prompts,
1204            baselines,
1205            store_version,
1206            migrated_to: None, // SQLite is never a migrated-from source
1207            dispenser: None,
1208        })
1209    }
1210
1211    fn save(&self, store: &RequirementsStore) -> Result<()> {
1212        let conn = self.conn.lock().unwrap();
1213
1214        // Use a transaction for atomicity
1215        conn.execute("BEGIN TRANSACTION", [])?;
1216
1217        // Clear existing data
1218        conn.execute("DELETE FROM requirements", [])?;
1219        conn.execute("DELETE FROM users", [])?;
1220
1221        // Save all requirements
1222        for req in &store.requirements {
1223            self.save_requirement(&conn, req)?;
1224        }
1225
1226        // Save all users
1227        for user in &store.users {
1228            self.save_user(&conn, user)?;
1229        }
1230
1231        // Save metadata
1232        self.save_metadata(&conn, store)?;
1233
1234        conn.execute("COMMIT", [])?;
1235        Ok(())
1236    }
1237
1238    fn update_atomically<F>(&self, update_fn: F) -> Result<RequirementsStore>
1239    where
1240        F: FnOnce(&mut RequirementsStore),
1241    {
1242        let conn = self.conn.lock().unwrap();
1243
1244        conn.execute("BEGIN EXCLUSIVE TRANSACTION", [])?;
1245
1246        // Load within transaction
1247        drop(conn);
1248        let mut store = self.load()?;
1249
1250        // Apply changes
1251        update_fn(&mut store);
1252
1253        // Save within transaction
1254        let conn = self.conn.lock().unwrap();
1255
1256        // Clear existing data
1257        conn.execute("DELETE FROM requirements", [])?;
1258        conn.execute("DELETE FROM users", [])?;
1259
1260        // Save all requirements
1261        for req in &store.requirements {
1262            self.save_requirement(&conn, req)?;
1263        }
1264
1265        // Save all users
1266        for user in &store.users {
1267            self.save_user(&conn, user)?;
1268        }
1269
1270        // Save metadata
1271        self.save_metadata(&conn, &store)?;
1272
1273        conn.execute("COMMIT", [])?;
1274        Ok(store)
1275    }
1276
1277    // Override for more efficient single-requirement operations
1278
1279    fn get_requirement(&self, id: &Uuid) -> Result<Option<Requirement>> {
1280        let conn = self.conn.lock().unwrap();
1281
1282        // Check if new columns exist (for schema migration compatibility)
1283        let has_trace_links = conn
1284            .query_row(
1285                "SELECT COUNT(*) FROM pragma_table_info('requirements') WHERE name='trace_links'",
1286                [],
1287                |row| row.get::<_, i32>(0),
1288            )
1289            .unwrap_or(0)
1290            > 0;
1291
1292        let query = if has_trace_links {
1293            "SELECT id, spec_id, prefix_override, title, description, status, priority,
1294                    owner, feature, created_at, created_by, modified_at, req_type,
1295                    dependencies, tags, relationships, comments, history, archived,
1296                    custom_status, custom_priority, custom_fields, urls, trace_links,
1297                    implementation_info, ai_evaluation, weight, attachments, gitlab_issues, version
1298             FROM requirements WHERE id = ?1"
1299        } else {
1300            "SELECT id, spec_id, prefix_override, title, description, status, priority,
1301                    owner, feature, created_at, created_by, modified_at, req_type,
1302                    dependencies, tags, relationships, comments, history, archived,
1303                    custom_status, custom_priority, custom_fields, urls, NULL as trace_links,
1304                    NULL as implementation_info, ai_evaluation, NULL as weight, '[]' as attachments, '[]' as gitlab_issues, version
1305             FROM requirements WHERE id = ?1"
1306        };
1307
1308        let result = conn
1309            .query_row(query, [id.to_string()], |row| {
1310                let id_str: String = row.get(0)?;
1311                let spec_id: Option<String> = row.get(1)?;
1312                let prefix_override: Option<String> = row.get(2)?;
1313                let title: String = row.get(3)?;
1314                let description: String = row.get(4)?;
1315                let status_str: String = row.get(5)?;
1316                let priority_str: String = row.get(6)?;
1317                let owner: String = row.get(7)?;
1318                let feature: String = row.get(8)?;
1319                let created_at_str: String = row.get(9)?;
1320                let created_by: Option<String> = row.get(10)?;
1321                let modified_at_str: String = row.get(11)?;
1322                let req_type_str: String = row.get(12)?;
1323                let dependencies_json: String = row.get(13)?;
1324                let tags_json: String = row.get(14)?;
1325                let relationships_json: String = row.get(15)?;
1326                let comments_json: String = row.get(16)?;
1327                let history_json: String = row.get(17)?;
1328                let archived: bool = row.get(18)?;
1329                let custom_status: Option<String> = row.get(19)?;
1330                let custom_priority: Option<String> = row.get(20)?;
1331                let custom_fields_json: String = row.get(21)?;
1332                let urls_json: String = row.get(22)?;
1333                let trace_links_json: Option<String> = row.get(23)?;
1334                let implementation_info_json: Option<String> = row.get(24)?;
1335                let ai_evaluation_json: Option<String> = row.get(25)?;
1336                let weight: Option<f32> = row.get(26)?;
1337                let attachments_json: String = row.get(27)?;
1338                let gitlab_issues_json: String = row.get(28)?;
1339                let version: i64 = row.get(29)?;
1340
1341                Ok((
1342                    id_str,
1343                    spec_id,
1344                    prefix_override,
1345                    title,
1346                    description,
1347                    status_str,
1348                    priority_str,
1349                    owner,
1350                    feature,
1351                    created_at_str,
1352                    created_by,
1353                    modified_at_str,
1354                    req_type_str,
1355                    dependencies_json,
1356                    tags_json,
1357                    relationships_json,
1358                    comments_json,
1359                    history_json,
1360                    archived,
1361                    custom_status,
1362                    custom_priority,
1363                    custom_fields_json,
1364                    urls_json,
1365                    trace_links_json,
1366                    implementation_info_json,
1367                    ai_evaluation_json,
1368                    weight,
1369                    attachments_json,
1370                    gitlab_issues_json,
1371                    version,
1372                ))
1373            })
1374            .optional()?;
1375
1376        match result {
1377            Some((
1378                id_str,
1379                spec_id,
1380                prefix_override,
1381                title,
1382                description,
1383                status_str,
1384                priority_str,
1385                owner,
1386                feature,
1387                created_at_str,
1388                created_by,
1389                modified_at_str,
1390                req_type_str,
1391                dependencies_json,
1392                tags_json,
1393                relationships_json,
1394                comments_json,
1395                history_json,
1396                archived,
1397                custom_status,
1398                custom_priority,
1399                custom_fields_json,
1400                urls_json,
1401                trace_links_json,
1402                implementation_info_json,
1403                ai_evaluation_json,
1404                weight,
1405                attachments_json,
1406                gitlab_issues_json,
1407                version,
1408            )) => {
1409                let id = Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::now_v7());
1410                let status = Self::str_to_status(&status_str);
1411                let priority = Self::str_to_priority(&priority_str);
1412                let req_type = Self::str_to_type(&req_type_str);
1413                let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
1414                    .map(|dt| dt.with_timezone(&chrono::Utc))
1415                    .unwrap_or_else(|_| chrono::Utc::now());
1416                let modified_at = chrono::DateTime::parse_from_rfc3339(&modified_at_str)
1417                    .map(|dt| dt.with_timezone(&chrono::Utc))
1418                    .unwrap_or_else(|_| chrono::Utc::now());
1419                let dependencies: Vec<Uuid> =
1420                    Self::from_json(&dependencies_json).unwrap_or_default();
1421                let tags: HashSet<String> = Self::from_json(&tags_json).unwrap_or_default();
1422                let relationships: Vec<Relationship> =
1423                    Self::from_json(&relationships_json).unwrap_or_default();
1424                let comments: Vec<Comment> = Self::from_json(&comments_json).unwrap_or_default();
1425                let history: Vec<HistoryEntry> = Self::from_json(&history_json).unwrap_or_default();
1426                let custom_fields: HashMap<String, String> =
1427                    Self::from_json(&custom_fields_json).unwrap_or_default();
1428                let urls: Vec<UrlLink> = Self::from_json(&urls_json).unwrap_or_default();
1429                let trace_links: Vec<TraceLink> = trace_links_json
1430                    .and_then(|json| Self::from_json(&json).ok())
1431                    .unwrap_or_default();
1432                let implementation_info: Option<ImplementationInfo> =
1433                    implementation_info_json.and_then(|json| Self::from_json(&json).ok());
1434                let ai_evaluation = ai_evaluation_json.and_then(|json| Self::from_json(&json).ok());
1435                let attachments: Vec<Attachment> =
1436                    Self::from_json(&attachments_json).unwrap_or_default();
1437                let gitlab_issues: Vec<GitLabIssueLink> =
1438                    Self::from_json(&gitlab_issues_json).unwrap_or_default();
1439
1440                Ok(Some(Requirement {
1441                    id,
1442                    spec_id,
1443                    agreed_id: None,
1444                    prefix_override,
1445                    title,
1446                    description,
1447                    status,
1448                    priority,
1449                    owner,
1450                    feature,
1451                    created_at,
1452                    created_by,
1453                    modified_at,
1454                    req_type,
1455                    meta_subtype: None, // Loaded separately if needed
1456                    dependencies,
1457                    tags,
1458                    weight,
1459                    relationships,
1460                    comments,
1461                    history,
1462                    archived,
1463                    custom_status,
1464                    custom_priority,
1465                    custom_fields,
1466                    urls,
1467                    attachments,
1468                    trace_links,
1469                    gitlab_issues,
1470                    implementation_info,
1471                    ai_evaluation,
1472                    version,
1473                }))
1474            }
1475            None => Ok(None),
1476        }
1477    }
1478
1479    fn update_requirement(&self, requirement: &Requirement) -> Result<()> {
1480        let conn = self.conn.lock().unwrap();
1481        self.save_requirement(&conn, requirement)
1482    }
1483
1484    fn update_requirement_versioned(
1485        &self,
1486        requirement: &Requirement,
1487    ) -> Result<super::traits::UpdateResult> {
1488        use super::traits::{UpdateResult, VersionConflict};
1489
1490        let conn = self.conn.lock().unwrap();
1491
1492        // Get current version from database
1493        let current_version: Option<i64> = conn
1494            .query_row(
1495                "SELECT version FROM requirements WHERE id = ?1",
1496                [requirement.id.to_string()],
1497                |row| row.get(0),
1498            )
1499            .optional()?;
1500
1501        match current_version {
1502            Some(db_version) => {
1503                // Check for version conflict
1504                if db_version != requirement.version {
1505                    return Ok(UpdateResult::Conflict(VersionConflict {
1506                        id: requirement.id,
1507                        expected_version: requirement.version,
1508                        current_version: db_version,
1509                        display_id: requirement
1510                            .spec_id
1511                            .clone()
1512                            .unwrap_or_else(|| requirement.id.to_string()),
1513                    }));
1514                }
1515
1516                // Version matches - update with incremented version
1517                let mut updated_req = requirement.clone();
1518                updated_req.version = db_version + 1;
1519                self.save_requirement(&conn, &updated_req)?;
1520
1521                Ok(UpdateResult::Success)
1522            }
1523            None => {
1524                anyhow::bail!("Requirement not found: {}", requirement.id)
1525            }
1526        }
1527    }
1528
1529    fn get_store_version(&self) -> Result<i64> {
1530        let conn = self.conn.lock().unwrap();
1531        self.load_store_version(&conn)
1532    }
1533
1534    fn delete_requirement(&self, id: &Uuid) -> Result<()> {
1535        let conn = self.conn.lock().unwrap();
1536        let rows_affected =
1537            conn.execute("DELETE FROM requirements WHERE id = ?1", [id.to_string()])?;
1538        if rows_affected == 0 {
1539            anyhow::bail!("Requirement not found: {}", id)
1540        }
1541        Ok(())
1542    }
1543
1544    fn get_user(&self, id: &Uuid) -> Result<Option<User>> {
1545        let conn = self.conn.lock().unwrap();
1546
1547        conn.query_row(
1548            "SELECT id, spec_id, name, email, handle, pin_hash, created_at, archived, version FROM users WHERE id = ?1",
1549            [id.to_string()],
1550            |row| {
1551                let id_str: String = row.get(0)?;
1552                let spec_id: Option<String> = row.get(1)?;
1553                let name: String = row.get(2)?;
1554                let email: String = row.get(3)?;
1555                let handle: String = row.get(4)?;
1556                let pin_hash: Option<String> = row.get(5)?;
1557                let created_at_str: String = row.get(6)?;
1558                let archived: bool = row.get(7)?;
1559                let version: i64 = row.get(8)?;
1560
1561                let id = Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::now_v7());
1562                let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
1563                    .map(|dt| dt.with_timezone(&chrono::Utc))
1564                    .unwrap_or_else(|_| chrono::Utc::now());
1565
1566                Ok(User {
1567                    id,
1568                    spec_id,
1569                    name,
1570                    email,
1571                    handle,
1572                    pin_hash,
1573                    created_at,
1574                    archived,
1575                    version,
1576                })
1577            }
1578        ).optional().map_err(|e| e.into())
1579    }
1580
1581    fn update_user(&self, user: &User) -> Result<()> {
1582        let conn = self.conn.lock().unwrap();
1583        self.save_user(&conn, user)
1584    }
1585
1586    fn delete_user(&self, id: &Uuid) -> Result<()> {
1587        let conn = self.conn.lock().unwrap();
1588        let rows_affected = conn.execute("DELETE FROM users WHERE id = ?1", [id.to_string()])?;
1589        if rows_affected == 0 {
1590            anyhow::bail!("User not found: {}", id)
1591        }
1592        Ok(())
1593    }
1594
1595    // =========================================================================
1596    // Queue Operations (STORY-0366)
1597    // =========================================================================
1598    // trace:STORY-0366 | ai:claude
1599
1600    fn queue_list(&self, user_id: &str, include_completed: bool) -> Result<Vec<QueueEntry>> {
1601        let conn = self.conn.lock().unwrap();
1602        let sql = if include_completed {
1603            "SELECT q.user_id, q.requirement_id, q.position, q.added_by, q.note, q.added_at \
1604             FROM queue_entries q \
1605             WHERE q.user_id = ?1 \
1606             ORDER BY q.position ASC"
1607        } else {
1608            "SELECT q.user_id, q.requirement_id, q.position, q.added_by, q.note, q.added_at \
1609             FROM queue_entries q \
1610             LEFT JOIN requirements r ON q.requirement_id = r.id \
1611             WHERE q.user_id = ?1 AND (r.status IS NULL OR r.status != 'Completed') \
1612             ORDER BY q.position ASC"
1613        };
1614
1615        let mut stmt = conn.prepare(sql)?;
1616        let entries = stmt
1617            .query_map([user_id], |row| {
1618                let user_id: String = row.get(0)?;
1619                let req_id_str: String = row.get(1)?;
1620                let position: i64 = row.get(2)?;
1621                let added_by: String = row.get(3)?;
1622                let note: Option<String> = row.get(4)?;
1623                let added_at_str: String = row.get(5)?;
1624
1625                let requirement_id =
1626                    Uuid::parse_str(&req_id_str).unwrap_or_else(|_| Uuid::now_v7());
1627                let added_at = chrono::DateTime::parse_from_rfc3339(&added_at_str)
1628                    .map(|dt| dt.with_timezone(&chrono::Utc))
1629                    .unwrap_or_else(|_| chrono::Utc::now());
1630
1631                Ok(QueueEntry {
1632                    user_id,
1633                    requirement_id,
1634                    position,
1635                    added_by,
1636                    note,
1637                    added_at,
1638                })
1639            })?
1640            .collect::<std::result::Result<Vec<_>, _>>()?;
1641
1642        Ok(entries)
1643    }
1644
1645    fn queue_add(&self, entry: QueueEntry) -> Result<()> {
1646        let conn = self.conn.lock().unwrap();
1647
1648        // Auto-assign position if i64::MAX (sentinel for "append to bottom")
1649        let position = if entry.position == i64::MAX {
1650            let max_pos: i64 = conn
1651                .query_row(
1652                    "SELECT COALESCE(MAX(position), 0) FROM queue_entries WHERE user_id = ?1",
1653                    [&entry.user_id],
1654                    |row| row.get(0),
1655                )
1656                .unwrap_or(0);
1657            max_pos + 1000
1658        } else {
1659            entry.position
1660        };
1661
1662        conn.execute(
1663            "INSERT OR REPLACE INTO queue_entries (user_id, requirement_id, position, added_by, note, added_at) \
1664             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1665            params![
1666                entry.user_id,
1667                entry.requirement_id.to_string(),
1668                position,
1669                entry.added_by,
1670                entry.note,
1671                entry.added_at.to_rfc3339(),
1672            ],
1673        )?;
1674        Ok(())
1675    }
1676
1677    fn queue_remove(&self, user_id: &str, requirement_id: &Uuid) -> Result<()> {
1678        let conn = self.conn.lock().unwrap();
1679        conn.execute(
1680            "DELETE FROM queue_entries WHERE user_id = ?1 AND requirement_id = ?2",
1681            params![user_id, requirement_id.to_string()],
1682        )?;
1683        Ok(())
1684    }
1685
1686    fn queue_reorder(&self, user_id: &str, items: &[(Uuid, i64)]) -> Result<()> {
1687        let conn = self.conn.lock().unwrap();
1688        let tx = conn.unchecked_transaction()?;
1689        for (req_id, position) in items {
1690            tx.execute(
1691                "UPDATE queue_entries SET position = ?1 WHERE user_id = ?2 AND requirement_id = ?3",
1692                params![position, user_id, req_id.to_string()],
1693            )?;
1694        }
1695        tx.commit()?;
1696        Ok(())
1697    }
1698
1699    fn queue_clear(&self, user_id: &str, completed_only: bool) -> Result<()> {
1700        let conn = self.conn.lock().unwrap();
1701        if completed_only {
1702            conn.execute(
1703                "DELETE FROM queue_entries WHERE user_id = ?1 AND requirement_id IN \
1704                 (SELECT id FROM requirements WHERE status = 'Completed')",
1705                [user_id],
1706            )?;
1707        } else {
1708            conn.execute("DELETE FROM queue_entries WHERE user_id = ?1", [user_id])?;
1709        }
1710        Ok(())
1711    }
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716    use super::*;
1717    use tempfile::NamedTempFile;
1718
1719    #[test]
1720    fn test_sqlite_backend_create_and_load() {
1721        let temp_file = NamedTempFile::with_suffix(".db").unwrap();
1722        let backend = SqliteBackend::new(temp_file.path()).unwrap();
1723
1724        backend.create_if_not_exists().unwrap();
1725
1726        let store = backend.load().unwrap();
1727        assert!(store.requirements.is_empty());
1728        assert!(store.users.is_empty());
1729    }
1730
1731    #[test]
1732    fn test_sqlite_backend_save_and_load() {
1733        let temp_file = NamedTempFile::with_suffix(".db").unwrap();
1734        let backend = SqliteBackend::new(temp_file.path()).unwrap();
1735
1736        let mut store = RequirementsStore::new();
1737        store.name = "Test DB".to_string();
1738        store.title = "Test Database".to_string();
1739
1740        backend.save(&store).unwrap();
1741
1742        let loaded = backend.load().unwrap();
1743        assert_eq!(loaded.name, "Test DB");
1744        assert_eq!(loaded.title, "Test Database");
1745    }
1746
1747    #[test]
1748    fn test_sqlite_backend_requirement_crud() {
1749        let temp_file = NamedTempFile::with_suffix(".db").unwrap();
1750        let backend = SqliteBackend::new(temp_file.path()).unwrap();
1751
1752        // Create initial store
1753        backend.save(&RequirementsStore::new()).unwrap();
1754
1755        // Add requirement
1756        let req = Requirement::new("Test Req".to_string(), "Test Description".to_string());
1757        let req = backend.add_requirement(req).unwrap();
1758
1759        // Get by ID
1760        let loaded = backend.get_requirement(&req.id).unwrap();
1761        assert!(loaded.is_some());
1762        assert_eq!(loaded.unwrap().title, "Test Req");
1763
1764        // Delete
1765        backend.delete_requirement(&req.id).unwrap();
1766        let loaded = backend.get_requirement(&req.id).unwrap();
1767        assert!(loaded.is_none());
1768    }
1769}