scitadel-db 0.6.0

SQLite-backed repositories and migrations for scitadel.
Documentation
use rusqlite::params;
use scitadel_core::error::CoreError;
use scitadel_core::models::{Assessment, AssessmentId, PaperId, QuestionId};
use scitadel_core::ports::AssessmentRepository;

use super::Database;
use crate::error::DbError;

pub struct SqliteAssessmentRepository {
    db: Database,
}

impl SqliteAssessmentRepository {
    pub fn new(db: Database) -> Self {
        Self { db }
    }
}

fn row_to_assessment(row: &rusqlite::Row) -> rusqlite::Result<Assessment> {
    let id: String = row.get("id")?;
    let paper_id: String = row.get("paper_id")?;
    let question_id: String = row.get("question_id")?;
    let created_at: String = row.get("created_at")?;

    Ok(Assessment {
        id: AssessmentId::from(id),
        paper_id: PaperId::from(paper_id),
        question_id: QuestionId::from(question_id),
        score: row.get("score")?,
        reasoning: row.get("reasoning")?,
        model: row.get("model")?,
        prompt: row.get("prompt")?,
        temperature: row.get("temperature")?,
        assessor: row.get("assessor")?,
        created_at: super::parse_rfc3339_or_now(&created_at),
    })
}

impl AssessmentRepository for SqliteAssessmentRepository {
    fn save(&self, assessment: &Assessment) -> Result<(), CoreError> {
        let conn = self.db.conn()?;
        conn.execute(
            "INSERT OR REPLACE INTO assessments
                (id, paper_id, question_id, score, reasoning, model,
                 prompt, temperature, assessor, created_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
            params![
                assessment.id.as_str(),
                assessment.paper_id.as_str(),
                assessment.question_id.as_str(),
                assessment.score,
                assessment.reasoning,
                assessment.model,
                assessment.prompt,
                assessment.temperature,
                assessment.assessor,
                assessment.created_at.to_rfc3339(),
            ],
        )
        .map_err(DbError::Sqlite)?;
        Ok(())
    }

    fn get_for_paper(
        &self,
        paper_id: &str,
        question_id: Option<&str>,
    ) -> Result<Vec<Assessment>, CoreError> {
        let conn = self.db.conn()?;
        let (sql, query_params): (&str, Vec<Box<dyn rusqlite::types::ToSql>>) =
            if let Some(qid) = question_id {
                (
                    "SELECT * FROM assessments WHERE paper_id = ?1 AND question_id = ?2",
                    vec![Box::new(paper_id.to_string()), Box::new(qid.to_string())],
                )
            } else {
                (
                    "SELECT * FROM assessments WHERE paper_id = ?1",
                    vec![Box::new(paper_id.to_string())],
                )
            };
        let mut stmt = conn.prepare(sql).map_err(DbError::Sqlite)?;
        let params: Vec<&dyn rusqlite::types::ToSql> =
            query_params.iter().map(|b| b.as_ref()).collect();
        let assessments = stmt
            .query_map(params.as_slice(), row_to_assessment)
            .map_err(DbError::Sqlite)?
            .filter_map(Result::ok)
            .collect();
        Ok(assessments)
    }

    fn get_for_question(&self, question_id: &str) -> Result<Vec<Assessment>, CoreError> {
        let conn = self.db.conn()?;
        let mut stmt = conn
            .prepare("SELECT * FROM assessments WHERE question_id = ?1")
            .map_err(DbError::Sqlite)?;
        let assessments = stmt
            .query_map(params![question_id], row_to_assessment)
            .map_err(DbError::Sqlite)?
            .filter_map(Result::ok)
            .collect();
        Ok(assessments)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sqlite::{Database, SqlitePaperRepository, SqliteQuestionRepository};
    use scitadel_core::models::{Paper, ResearchQuestion};
    use scitadel_core::ports::{PaperRepository, QuestionRepository};

    #[test]
    fn test_assessment_crud() {
        let db = Database::open_in_memory().unwrap();
        db.migrate().unwrap();
        let paper_repo = SqlitePaperRepository::new(db.clone());
        let q_repo = SqliteQuestionRepository::new(db.clone());
        let a_repo = SqliteAssessmentRepository::new(db);

        let paper = Paper::new("Test Paper");
        paper_repo.save(&paper).unwrap();

        let q = ResearchQuestion::new("Test question");
        q_repo.save_question(&q).unwrap();

        let assessment = Assessment::new(paper.id.clone(), q.id.clone(), 0.85);
        a_repo.save(&assessment).unwrap();

        let loaded = a_repo
            .get_for_paper(paper.id.as_str(), Some(q.id.as_str()))
            .unwrap();
        assert_eq!(loaded.len(), 1);
        assert!((loaded[0].score - 0.85).abs() < f64::EPSILON);
    }
}