Skip to main content

kobo_db_tools/
db.rs

1use rusqlite::Connection;
2use thiserror::Error;
3
4const TRIGGER_NAME: &str = "prevent_delete_on_analytics_events";
5
6#[derive(Debug, Error)]
7pub enum TriggerError {
8    #[error("trigger already exists: {0}")]
9    AlreadyExists(&'static str),
10    #[error("trigger does not exist: {0}")]
11    NotFound(&'static str),
12    #[error("sqlite error: {0}")]
13    Sqlite(#[from] rusqlite::Error),
14}
15
16fn trigger_exists(conn: &Connection) -> rusqlite::Result<bool> {
17    let mut stmt =
18        conn.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' AND name = ?1 LIMIT 1;")?;
19    let mut rows = stmt.query([TRIGGER_NAME])?;
20    Ok(rows.next()?.is_some())
21}
22
23pub fn install_analytics_events_trigger(conn: &Connection) -> Result<(), TriggerError> {
24    if trigger_exists(conn)? {
25        return Err(TriggerError::AlreadyExists(TRIGGER_NAME));
26    }
27    conn.execute_batch(
28        "CREATE TRIGGER prevent_delete_on_analytics_events
29         BEFORE DELETE ON AnalyticsEvents
30         BEGIN
31           SELECT RAISE(ABORT, 'Deletion is not allowed on AnalyticsEvents table');
32         END;",
33    )?;
34    Ok(())
35}
36
37pub fn remove_analytics_events_trigger(conn: &Connection) -> Result<(), TriggerError> {
38    if !trigger_exists(conn)? {
39        return Err(TriggerError::NotFound(TRIGGER_NAME));
40    }
41    conn.execute_batch("DROP TRIGGER prevent_delete_on_analytics_events;")?;
42    Ok(())
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    fn setup_test_db() -> Connection {
50        let conn = Connection::open_in_memory().unwrap();
51        conn.execute_batch(
52            "CREATE TABLE AnalyticsEvents (
53                Id TEXT PRIMARY KEY,
54                Type TEXT NOT NULL,
55                Timestamp TEXT NOT NULL,
56                Attributes TEXT,
57                Metrics TEXT
58            );",
59        )
60        .unwrap();
61        conn
62    }
63
64    #[test]
65    fn test_trigger_install_and_delete_block() {
66        let conn = setup_test_db();
67        install_analytics_events_trigger(&conn).unwrap();
68        conn.execute(
69            "INSERT INTO AnalyticsEvents (Id, Type, Timestamp, Attributes, Metrics) VALUES (?1, ?2, ?3, ?4, ?5)",
70            ["row1", "AppStart", "2023-01-01T00:00:00Z", "", ""],
71        )
72        .unwrap();
73        let err = conn.execute("DELETE FROM AnalyticsEvents", []).unwrap_err();
74        let err_msg = err.to_string();
75        assert!(err_msg.contains("Deletion is not allowed"));
76    }
77
78    #[test]
79    fn test_trigger_remove() {
80        let conn = setup_test_db();
81        install_analytics_events_trigger(&conn).unwrap();
82        remove_analytics_events_trigger(&conn).unwrap();
83        conn.execute("DELETE FROM AnalyticsEvents", []).unwrap();
84    }
85
86    #[test]
87    fn test_trigger_install_existing() {
88        let conn = setup_test_db();
89        install_analytics_events_trigger(&conn).unwrap();
90        let err = install_analytics_events_trigger(&conn).unwrap_err();
91        assert!(matches!(err, TriggerError::AlreadyExists(_)));
92    }
93
94    #[test]
95    fn test_trigger_remove_missing() {
96        let conn = setup_test_db();
97        let err = remove_analytics_events_trigger(&conn).unwrap_err();
98        assert!(matches!(err, TriggerError::NotFound(_)));
99    }
100}