kobo-db-tools 0.0.13

A Rust library to parse and analyze Kobo e-reader SQLite databases.
Documentation
use rusqlite::Connection;
use thiserror::Error;

const TRIGGER_NAME: &str = "prevent_delete_on_analytics_events";

#[derive(Debug, Error)]
pub enum TriggerError {
    #[error("trigger already exists: {0}")]
    AlreadyExists(&'static str),
    #[error("trigger does not exist: {0}")]
    NotFound(&'static str),
    #[error("sqlite error: {0}")]
    Sqlite(#[from] rusqlite::Error),
}

fn trigger_exists(conn: &Connection) -> rusqlite::Result<bool> {
    let mut stmt =
        conn.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' AND name = ?1 LIMIT 1;")?;
    let mut rows = stmt.query([TRIGGER_NAME])?;
    Ok(rows.next()?.is_some())
}

pub fn install_analytics_events_trigger(conn: &Connection) -> Result<(), TriggerError> {
    if trigger_exists(conn)? {
        return Err(TriggerError::AlreadyExists(TRIGGER_NAME));
    }
    conn.execute_batch(
        "CREATE TRIGGER prevent_delete_on_analytics_events
         BEFORE DELETE ON AnalyticsEvents
         BEGIN
           SELECT RAISE(ABORT, 'Deletion is not allowed on AnalyticsEvents table');
         END;",
    )?;
    Ok(())
}

pub fn remove_analytics_events_trigger(conn: &Connection) -> Result<(), TriggerError> {
    if !trigger_exists(conn)? {
        return Err(TriggerError::NotFound(TRIGGER_NAME));
    }
    conn.execute_batch("DROP TRIGGER prevent_delete_on_analytics_events;")?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn setup_test_db() -> Connection {
        let conn = Connection::open_in_memory().unwrap();
        conn.execute_batch(
            "CREATE TABLE AnalyticsEvents (
                Id TEXT PRIMARY KEY,
                Type TEXT NOT NULL,
                Timestamp TEXT NOT NULL,
                Attributes TEXT,
                Metrics TEXT
            );",
        )
        .unwrap();
        conn
    }

    #[test]
    fn test_trigger_install_and_delete_block() {
        let conn = setup_test_db();
        install_analytics_events_trigger(&conn).unwrap();
        conn.execute(
            "INSERT INTO AnalyticsEvents (Id, Type, Timestamp, Attributes, Metrics) VALUES (?1, ?2, ?3, ?4, ?5)",
            ["row1", "AppStart", "2023-01-01T00:00:00Z", "", ""],
        )
        .unwrap();
        let err = conn.execute("DELETE FROM AnalyticsEvents", []).unwrap_err();
        let err_msg = err.to_string();
        assert!(err_msg.contains("Deletion is not allowed"));
    }

    #[test]
    fn test_trigger_remove() {
        let conn = setup_test_db();
        install_analytics_events_trigger(&conn).unwrap();
        remove_analytics_events_trigger(&conn).unwrap();
        conn.execute("DELETE FROM AnalyticsEvents", []).unwrap();
    }

    #[test]
    fn test_trigger_install_existing() {
        let conn = setup_test_db();
        install_analytics_events_trigger(&conn).unwrap();
        let err = install_analytics_events_trigger(&conn).unwrap_err();
        assert!(matches!(err, TriggerError::AlreadyExists(_)));
    }

    #[test]
    fn test_trigger_remove_missing() {
        let conn = setup_test_db();
        let err = remove_analytics_events_trigger(&conn).unwrap_err();
        assert!(matches!(err, TriggerError::NotFound(_)));
    }
}