metis_core/dal/database/
repository.rs

1use crate::dal::database::models::*;
2use crate::dal::database::schema;
3use crate::{MetisError, Result};
4use diesel::prelude::*;
5use diesel::sqlite::SqliteConnection;
6
7/// Data access repository for document operations
8pub struct DocumentRepository {
9    connection: SqliteConnection,
10}
11
12impl DocumentRepository {
13    pub fn new(connection: SqliteConnection) -> Self {
14        Self { connection }
15    }
16
17    /// Insert a new document into the database
18    pub fn create_document(&mut self, doc: NewDocument) -> Result<Document> {
19        use schema::documents::dsl::*;
20
21        diesel::insert_into(documents)
22            .values(&doc)
23            .returning(Document::as_returning())
24            .get_result(&mut self.connection)
25            .map_err(MetisError::Database)
26    }
27
28    /// Find a document by its filepath
29    pub fn find_by_filepath(&mut self, file_path: &str) -> Result<Option<Document>> {
30        use schema::documents::dsl::*;
31
32        documents
33            .filter(filepath.eq(file_path))
34            .first(&mut self.connection)
35            .optional()
36            .map_err(MetisError::Database)
37    }
38
39    /// Find a document by its ID
40    pub fn find_by_id(&mut self, document_id: &str) -> Result<Option<Document>> {
41        use schema::documents::dsl::*;
42
43        documents
44            .filter(id.eq(document_id))
45            .first(&mut self.connection)
46            .optional()
47            .map_err(MetisError::Database)
48    }
49
50    /// Update an existing document
51    pub fn update_document(&mut self, file_path: &str, doc: &Document) -> Result<Document> {
52        use schema::documents::dsl::*;
53
54        diesel::update(documents.filter(filepath.eq(file_path)))
55            .set(doc)
56            .returning(Document::as_returning())
57            .get_result(&mut self.connection)
58            .map_err(MetisError::Database)
59    }
60
61    /// Delete a document and all its relationships
62    pub fn delete_document(&mut self, file_path: &str) -> Result<bool> {
63        use schema::documents::dsl::*;
64
65        let deleted_count = diesel::delete(documents.filter(filepath.eq(file_path)))
66            .execute(&mut self.connection)
67            .map_err(MetisError::Database)?;
68
69        Ok(deleted_count > 0)
70    }
71
72    /// Find all children of a document
73    pub fn find_children(&mut self, parent_document_id: &str) -> Result<Vec<Document>> {
74        use schema::document_relationships::dsl::*;
75        use schema::documents::dsl::*;
76
77        documents
78            .inner_join(document_relationships.on(id.eq(child_id)))
79            .filter(parent_id.eq(parent_document_id))
80            .select(Document::as_select())
81            .load(&mut self.connection)
82            .map_err(MetisError::Database)
83    }
84
85    /// Find the parent of a document
86    pub fn find_parent(&mut self, child_document_id: &str) -> Result<Option<Document>> {
87        use schema::document_relationships::dsl::*;
88        use schema::documents::dsl::*;
89
90        documents
91            .inner_join(document_relationships.on(id.eq(parent_id)))
92            .filter(child_id.eq(child_document_id))
93            .select(Document::as_select())
94            .first(&mut self.connection)
95            .optional()
96            .map_err(MetisError::Database)
97    }
98
99    /// Create a parent-child relationship
100    pub fn create_relationship(&mut self, relationship: DocumentRelationship) -> Result<()> {
101        use schema::document_relationships::dsl::*;
102
103        diesel::insert_into(document_relationships)
104            .values(&relationship)
105            .execute(&mut self.connection)
106            .map_err(MetisError::Database)?;
107
108        Ok(())
109    }
110
111    /// Search documents using FTS
112    pub fn search_documents(&mut self, query: &str) -> Result<Vec<Document>> {
113        // For SQLite FTS, we need to use sql_query for the MATCH operator
114        diesel::sql_query(
115            "
116            SELECT d.* FROM documents d
117            INNER JOIN document_search ds ON d.filepath = ds.document_filepath
118            WHERE document_search MATCH ?
119        ",
120        )
121        .bind::<diesel::sql_types::Text, _>(query)
122        .load::<Document>(&mut self.connection)
123        .map_err(MetisError::Database)
124    }
125
126    /// Get all documents of a specific type
127    pub fn find_by_type(&mut self, doc_type: &str) -> Result<Vec<Document>> {
128        use schema::documents::dsl::*;
129
130        documents
131            .filter(document_type.eq(doc_type))
132            .order(updated_at.desc())
133            .load(&mut self.connection)
134            .map_err(MetisError::Database)
135    }
136
137    /// Get documents with specific tags
138    pub fn find_by_tag(&mut self, tag_name: &str) -> Result<Vec<Document>> {
139        use schema::document_tags::dsl::*;
140        use schema::documents::dsl::*;
141
142        documents
143            .inner_join(document_tags.on(filepath.eq(document_filepath)))
144            .filter(tag.eq(tag_name))
145            .select(Document::as_select())
146            .load(&mut self.connection)
147            .map_err(MetisError::Database)
148    }
149
150    /// Get documents in a specific phase
151    pub fn find_by_phase(&mut self, phase_name: &str) -> Result<Vec<Document>> {
152        use schema::documents::dsl::*;
153
154        documents
155            .filter(phase.eq(phase_name))
156            .order(updated_at.desc())
157            .load(&mut self.connection)
158            .map_err(MetisError::Database)
159    }
160
161    /// Get documents by type and phase
162    pub fn find_by_type_and_phase(
163        &mut self,
164        doc_type: &str,
165        phase_name: &str,
166    ) -> Result<Vec<Document>> {
167        use schema::documents::dsl::*;
168
169        documents
170            .filter(document_type.eq(doc_type))
171            .filter(phase.eq(phase_name))
172            .order(updated_at.desc())
173            .load(&mut self.connection)
174            .map_err(MetisError::Database)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::dal::Database;
182
183    fn setup_test_repository() -> DocumentRepository {
184        let db = Database::new(":memory:").expect("Failed to create test database");
185        db.into_repository()
186    }
187
188    fn create_test_document() -> NewDocument {
189        NewDocument {
190            filepath: "/test/doc.md".to_string(),
191            id: "test-doc-1".to_string(),
192            title: "Test Document".to_string(),
193            document_type: "vision".to_string(),
194            created_at: 1609459200.0, // 2021-01-01
195            updated_at: 1609459200.0,
196            archived: false,
197            exit_criteria_met: false,
198            file_hash: "abc123".to_string(),
199            frontmatter_json: "{}".to_string(),
200            content: Some("Test content".to_string()),
201            phase: "draft".to_string(),
202        }
203    }
204
205    #[test]
206    fn test_create_and_find_document() {
207        let mut repo = setup_test_repository();
208
209        let new_doc = create_test_document();
210        let created = repo
211            .create_document(new_doc)
212            .expect("Failed to create document");
213
214        assert_eq!(created.filepath, "/test/doc.md");
215        assert_eq!(created.title, "Test Document");
216        assert_eq!(created.document_type, "vision");
217
218        // Test find by filepath
219        let found = repo
220            .find_by_filepath("/test/doc.md")
221            .expect("Failed to find document")
222            .expect("Document not found");
223        assert_eq!(found.id, "test-doc-1");
224
225        // Test find by id
226        let found_by_id = repo
227            .find_by_id("test-doc-1")
228            .expect("Failed to find document")
229            .expect("Document not found");
230        assert_eq!(found_by_id.filepath, "/test/doc.md");
231    }
232
233    #[test]
234    fn test_update_document() {
235        let mut repo = setup_test_repository();
236
237        let new_doc = create_test_document();
238        let mut created = repo
239            .create_document(new_doc)
240            .expect("Failed to create document");
241
242        // Update the document
243        created.title = "Updated Title".to_string();
244        created.updated_at = 1609462800.0; // 1 hour later
245
246        let updated = repo
247            .update_document("/test/doc.md", &created)
248            .expect("Failed to update document");
249
250        assert_eq!(updated.title, "Updated Title");
251        assert_eq!(updated.updated_at, 1609462800.0);
252    }
253
254    #[test]
255    fn test_delete_document() {
256        let mut repo = setup_test_repository();
257
258        let new_doc = create_test_document();
259        repo.create_document(new_doc)
260            .expect("Failed to create document");
261
262        // Delete the document
263        let deleted = repo
264            .delete_document("/test/doc.md")
265            .expect("Failed to delete document");
266        assert!(deleted);
267
268        // Verify it's gone
269        let found = repo
270            .find_by_filepath("/test/doc.md")
271            .expect("Failed to search for document");
272        assert!(found.is_none());
273
274        // Try to delete non-existent document
275        let deleted_again = repo
276            .delete_document("/test/doc.md")
277            .expect("Failed to delete document");
278        assert!(!deleted_again);
279    }
280
281    #[test]
282    fn test_document_relationships() {
283        let mut repo = setup_test_repository();
284
285        // Create parent document
286        let parent_doc = NewDocument {
287            filepath: "/parent.md".to_string(),
288            id: "parent-1".to_string(),
289            title: "Parent Document".to_string(),
290            document_type: "strategy".to_string(),
291            created_at: 1609459200.0,
292            updated_at: 1609459200.0,
293            archived: false,
294            exit_criteria_met: false,
295            file_hash: "parent123".to_string(),
296            frontmatter_json: "{}".to_string(),
297            content: Some("Parent content".to_string()),
298            phase: "shaping".to_string(),
299        };
300        repo.create_document(parent_doc)
301            .expect("Failed to create parent");
302
303        // Create child document
304        let child_doc = NewDocument {
305            filepath: "/child.md".to_string(),
306            id: "child-1".to_string(),
307            title: "Child Document".to_string(),
308            document_type: "initiative".to_string(),
309            created_at: 1609459200.0,
310            updated_at: 1609459200.0,
311            archived: false,
312            exit_criteria_met: false,
313            file_hash: "child123".to_string(),
314            frontmatter_json: "{}".to_string(),
315            content: Some("Child content".to_string()),
316            phase: "discovery".to_string(),
317        };
318        repo.create_document(child_doc)
319            .expect("Failed to create child");
320
321        // Create relationship
322        let relationship = DocumentRelationship {
323            child_id: "child-1".to_string(),
324            parent_id: "parent-1".to_string(),
325            child_filepath: "/child.md".to_string(),
326            parent_filepath: "/parent.md".to_string(),
327        };
328        repo.create_relationship(relationship)
329            .expect("Failed to create relationship");
330
331        // Test find children
332        let children = repo
333            .find_children("parent-1")
334            .expect("Failed to find children");
335        assert_eq!(children.len(), 1);
336        assert_eq!(children[0].id, "child-1");
337
338        // Test find parent
339        let parent = repo
340            .find_parent("child-1")
341            .expect("Failed to find parent")
342            .expect("Parent not found");
343        assert_eq!(parent.id, "parent-1");
344    }
345
346    #[test]
347    fn test_find_by_type() {
348        let mut repo = setup_test_repository();
349
350        // Create documents of different types
351        let vision_doc = NewDocument {
352            document_type: "vision".to_string(),
353            filepath: "/vision.md".to_string(),
354            id: "vision-1".to_string(),
355            title: "Vision Doc".to_string(),
356            created_at: 1609459200.0,
357            updated_at: 1609459200.0,
358            archived: false,
359            exit_criteria_met: false,
360            file_hash: "vision123".to_string(),
361            frontmatter_json: "{}".to_string(),
362            content: None,
363            phase: "draft".to_string(),
364        };
365
366        let strategy_doc = NewDocument {
367            document_type: "strategy".to_string(),
368            filepath: "/strategy.md".to_string(),
369            id: "strategy-1".to_string(),
370            title: "Strategy Doc".to_string(),
371            created_at: 1609462800.0, // Later timestamp
372            updated_at: 1609462800.0,
373            archived: false,
374            exit_criteria_met: false,
375            file_hash: "strategy123".to_string(),
376            frontmatter_json: "{}".to_string(),
377            content: None,
378            phase: "shaping".to_string(),
379        };
380
381        repo.create_document(vision_doc)
382            .expect("Failed to create vision");
383        repo.create_document(strategy_doc)
384            .expect("Failed to create strategy");
385
386        // Test find by type
387        let visions = repo.find_by_type("vision").expect("Failed to find visions");
388        assert_eq!(visions.len(), 1);
389        assert_eq!(visions[0].document_type, "vision");
390
391        let strategies = repo
392            .find_by_type("strategy")
393            .expect("Failed to find strategies");
394        assert_eq!(strategies.len(), 1);
395        assert_eq!(strategies[0].document_type, "strategy");
396
397        // Verify ordering (newest first)
398        let _all_docs = repo.find_by_type("vision").expect("Failed to find docs");
399        // Since we only have one vision, we can't test ordering here
400        // But the query should work
401    }
402
403    #[test]
404    fn test_document_not_found() {
405        let mut repo = setup_test_repository();
406
407        let found = repo
408            .find_by_filepath("/nonexistent.md")
409            .expect("Failed to search for document");
410        assert!(found.is_none());
411
412        let found_by_id = repo
413            .find_by_id("nonexistent")
414            .expect("Failed to search for document");
415        assert!(found_by_id.is_none());
416    }
417}