Skip to main content

scitadel_db/sqlite/
questions.rs

1use rusqlite::{OptionalExtension, params};
2use scitadel_core::error::CoreError;
3use scitadel_core::models::{QuestionId, ResearchQuestion, SearchTerm, SearchTermId};
4use scitadel_core::ports::QuestionRepository;
5
6use super::Database;
7use crate::error::DbError;
8
9pub struct SqliteQuestionRepository {
10    db: Database,
11}
12
13impl SqliteQuestionRepository {
14    pub fn new(db: Database) -> Self {
15        Self { db }
16    }
17}
18
19impl QuestionRepository for SqliteQuestionRepository {
20    fn save_question(&self, question: &ResearchQuestion) -> Result<(), CoreError> {
21        let conn = self.db.conn()?;
22        conn.execute(
23            "INSERT OR REPLACE INTO research_questions
24                (id, text, description, created_at, updated_at)
25             VALUES (?1, ?2, ?3, ?4, ?5)",
26            params![
27                question.id.as_str(),
28                question.text,
29                question.description,
30                question.created_at.to_rfc3339(),
31                question.updated_at.to_rfc3339(),
32            ],
33        )
34        .map_err(DbError::Sqlite)?;
35        Ok(())
36    }
37
38    fn get_question(&self, question_id: &str) -> Result<Option<ResearchQuestion>, CoreError> {
39        let conn = self.db.conn()?;
40        let mut stmt = conn
41            .prepare("SELECT * FROM research_questions WHERE id = ?1")
42            .map_err(DbError::Sqlite)?;
43        let result = stmt
44            .query_row(params![question_id], |row| {
45                let id: String = row.get("id")?;
46                let created_at: String = row.get("created_at")?;
47                let updated_at: String = row.get("updated_at")?;
48                Ok(ResearchQuestion {
49                    id: QuestionId::from(id),
50                    text: row.get("text")?,
51                    description: row.get("description")?,
52                    created_at: super::parse_rfc3339_or_now(&created_at),
53                    updated_at: super::parse_rfc3339_or_now(&updated_at),
54                })
55            })
56            .optional()
57            .map_err(DbError::Sqlite)?;
58        Ok(result)
59    }
60
61    fn list_questions(&self) -> Result<Vec<ResearchQuestion>, CoreError> {
62        let conn = self.db.conn()?;
63        let mut stmt = conn
64            .prepare("SELECT * FROM research_questions ORDER BY created_at DESC")
65            .map_err(DbError::Sqlite)?;
66        let questions = stmt
67            .query_map([], |row| {
68                let id: String = row.get("id")?;
69                let created_at: String = row.get("created_at")?;
70                let updated_at: String = row.get("updated_at")?;
71                Ok(ResearchQuestion {
72                    id: QuestionId::from(id),
73                    text: row.get("text")?,
74                    description: row.get("description")?,
75                    created_at: super::parse_rfc3339_or_now(&created_at),
76                    updated_at: super::parse_rfc3339_or_now(&updated_at),
77                })
78            })
79            .map_err(DbError::Sqlite)?
80            .filter_map(Result::ok)
81            .collect();
82        Ok(questions)
83    }
84
85    fn save_term(&self, term: &SearchTerm) -> Result<(), CoreError> {
86        let conn = self.db.conn()?;
87        conn.execute(
88            "INSERT OR REPLACE INTO search_terms
89                (id, question_id, terms, query_string, created_at)
90             VALUES (?1, ?2, ?3, ?4, ?5)",
91            params![
92                term.id.as_str(),
93                term.question_id.as_str(),
94                serde_json::to_string(&term.terms).unwrap_or_default(),
95                term.query_string,
96                term.created_at.to_rfc3339(),
97            ],
98        )
99        .map_err(DbError::Sqlite)?;
100        Ok(())
101    }
102
103    fn get_terms(&self, question_id: &str) -> Result<Vec<SearchTerm>, CoreError> {
104        let conn = self.db.conn()?;
105        let mut stmt = conn
106            .prepare("SELECT * FROM search_terms WHERE question_id = ?1")
107            .map_err(DbError::Sqlite)?;
108        let terms = stmt
109            .query_map(params![question_id], |row| {
110                let id: String = row.get("id")?;
111                let question_id: String = row.get("question_id")?;
112                let terms_json: String = row.get("terms")?;
113                let created_at: String = row.get("created_at")?;
114                Ok(SearchTerm {
115                    id: SearchTermId::from(id),
116                    question_id: QuestionId::from(question_id),
117                    terms: serde_json::from_str(&terms_json).unwrap_or_default(),
118                    query_string: row.get("query_string")?,
119                    created_at: super::parse_rfc3339_or_now(&created_at),
120                })
121            })
122            .map_err(DbError::Sqlite)?
123            .filter_map(Result::ok)
124            .collect();
125        Ok(terms)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::sqlite::Database;
133
134    #[test]
135    fn test_question_crud() {
136        let db = Database::open_in_memory().unwrap();
137        db.migrate().unwrap();
138        let repo = SqliteQuestionRepository::new(db);
139
140        let q = ResearchQuestion::new("What is dark matter?");
141        repo.save_question(&q).unwrap();
142
143        let loaded = repo.get_question(q.id.as_str()).unwrap().unwrap();
144        assert_eq!(loaded.text, "What is dark matter?");
145
146        let all = repo.list_questions().unwrap();
147        assert_eq!(all.len(), 1);
148    }
149
150    #[test]
151    fn test_search_terms() {
152        let db = Database::open_in_memory().unwrap();
153        db.migrate().unwrap();
154        let repo = SqliteQuestionRepository::new(db);
155
156        let q = ResearchQuestion::new("Test question");
157        repo.save_question(&q).unwrap();
158
159        let mut term = SearchTerm::new(q.id.clone());
160        term.terms = vec!["dark".into(), "matter".into()];
161        term.query_string = "dark matter".into();
162        repo.save_term(&term).unwrap();
163
164        let terms = repo.get_terms(q.id.as_str()).unwrap();
165        assert_eq!(terms.len(), 1);
166        assert_eq!(terms[0].query_string, "dark matter");
167    }
168}