use std::error::Error; use log::debug; use rusqlite::{params, Connection, OptionalExtension}; /// Struct for each query line #[derive(Debug)] pub struct TweetToToot { pub twitter_screen_name: String, pub tweet_id: u64, pub toot_id: String, } /// if None is passed, read the last tweet from DB /// if a tweet_id is passed, read this particular tweet from DB pub fn read_state( conn: &Connection, n: &str, s: Option, ) -> Result, Box> { debug!("Reading tweet_id {:?}", s); let query: String = match s { Some(i) => format!("SELECT * FROM tweet_to_toot WHERE tweet_id = {} and twitter_screen_name = \"{}\"", i, n), None => format!("SELECT * FROM tweet_to_toot WHERE twitter_screen_name = \"{}\" ORDER BY tweet_id DESC LIMIT 1", n), }; let mut stmt = conn.prepare(&query)?; let t = stmt .query_row([], |row| { Ok(TweetToToot { twitter_screen_name: row.get("twitter_screen_name")?, tweet_id: row.get("tweet_id")?, toot_id: row.get("toot_id")?, }) }) .optional()?; Ok(t) } /// Writes last treated tweet id and toot id to the db pub fn write_state(conn: &Connection, t: TweetToToot) -> Result<(), Box> { debug!("Write struct {:?}", t); conn.execute( "INSERT INTO tweet_to_toot (twitter_screen_name, tweet_id, toot_id) VALUES (?1, ?2, ?3)", params![t.twitter_screen_name, t.tweet_id, t.toot_id], )?; Ok(()) } /// Initiates the DB from path pub fn init_db(d: &str) -> Result<(), Box> { debug!("Initializing DB for Scootaloo"); let conn = Connection::open(d)?; conn.execute( "CREATE TABLE IF NOT EXISTS tweet_to_toot ( twitter_screen_name TEXT NOT NULL, tweet_id INTEGER PRIMARY KEY, toot_id TEXT UNIQUE )", [], )?; Ok(()) } /// Migrate DB from 0.6.x to 0.7.x pub fn migrate_db(d: &str, s: &str) -> Result<(), Box> { debug!("Migrating DB for Scootaloo"); let conn = Connection::open(d)?; let res = conn.execute( &format!( "ALTER TABLE tweet_to_toot ADD COLUMN twitter_screen_name TEXT NOT NULL DEFAULT \"{}\"", s ), [], ); match res { Err(e) => match e.to_string().as_str() { "duplicate column name: twitter_screen_name" => Ok(()), _ => Err(e.into()), }, _ => Ok(()), } } #[cfg(test)] mod tests { use super::*; use std::{fs::remove_file, path::Path}; #[test] fn test_init_db() { let d = "/tmp/test_init_db.sqlite"; init_db(d).unwrap(); // check that file exist assert!(Path::new(d).exists()); // open said file let conn = Connection::open(d).unwrap(); conn.execute("SELECT * from tweet_to_toot;", []).unwrap(); remove_file(d).unwrap(); } #[test] fn test_init_init_db() { // init_db fn should be idempotent so let’s test that let d = "/tmp/test_init_init_db.sqlite"; init_db(d).unwrap(); let conn = Connection::open(d).unwrap(); conn.execute( "INSERT INTO tweet_to_toot (twitter_screen_name, tweet_id, toot_id) VALUES ('tamerelol', 100, 'A');", [], ) .unwrap(); init_db(d).unwrap(); remove_file(d).unwrap(); } #[test] fn test_write_state() { let d = "/tmp/test_write_state.sqlite"; init_db(d).unwrap(); let conn = Connection::open(d).unwrap(); let t_in = TweetToToot { twitter_screen_name: "tamerelol".to_string(), tweet_id: 123456789, toot_id: "987654321".to_string(), }; write_state(&conn, t_in).unwrap(); let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot;").unwrap(); let t_out = stmt .query_row([], |row| { Ok(TweetToToot { twitter_screen_name: row.get("twitter_screen_name").unwrap(), tweet_id: row.get("tweet_id").unwrap(), toot_id: row.get("toot_id").unwrap(), }) }) .unwrap(); assert_eq!(&t_out.twitter_screen_name, "tamerelol"); assert_eq!(t_out.tweet_id, 123456789); assert_eq!(&t_out.toot_id, "987654321"); remove_file(d).unwrap(); } #[test] fn test_none_to_tweet_id_read_state() { let d = "/tmp/test_none_to_tweet_id_read_state.sqlite"; init_db(d).unwrap(); let conn = Connection::open(d).unwrap(); conn.execute( "INSERT INTO tweet_to_toot (twitter_screen_name, tweet_id, toot_id) VALUES ('tamerelol', 101, 'A'), ('tamerelol', 102, 'B');", [], ) .unwrap(); let t_out = read_state(&conn, "tamerelol", None).unwrap().unwrap(); remove_file(d).unwrap(); assert_eq!(t_out.tweet_id, 102); assert_eq!(t_out.toot_id, "B"); } #[test] fn test_none_to_none_read_state() { let d = "/tmp/test_none_to_none_read_state.sqlite"; init_db(d).unwrap(); let conn = Connection::open(d).unwrap(); let t_out = read_state(&conn, "tamerelol", None).unwrap(); remove_file(d).unwrap(); assert!(t_out.is_none()); } #[test] fn test_tweet_id_to_none_read_state() { let d = "/tmp/test_tweet_id_to_none_read_state.sqlite"; init_db(d).unwrap(); let conn = Connection::open(d).unwrap(); conn.execute( "INSERT INTO tweet_to_toot (twitter_screen_name, tweet_id, toot_id) VALUES ('tamerelol', 100, 'A');", [], ) .unwrap(); let t_out = read_state(&conn, "tamerelol", Some(101)).unwrap(); remove_file(d).unwrap(); assert!(t_out.is_none()); } #[test] fn test_tweet_id_to_tweet_id_read_state() { let d = "/tmp/test_tweet_id_to_tweet_id_read_state.sqlite"; init_db(d).unwrap(); let conn = Connection::open(d).unwrap(); conn.execute( "INSERT INTO tweet_to_toot (twitter_screen_name, tweet_id, toot_id) VALUES ('tamerelol', 100, 'A');", [], ) .unwrap(); let t_out = read_state(&conn, "tamerelol", Some(100)).unwrap().unwrap(); remove_file(d).unwrap(); assert_eq!(t_out.tweet_id, 100); assert_eq!(t_out.toot_id, "A"); } #[test] fn test_migrate_db_add_column() { let d = "/tmp/test_migrate_db_add_column.sqlite"; let conn = Connection::open(d).unwrap(); conn.execute( "CREATE TABLE IF NOT EXISTS tweet_to_toot ( tweet_id INTEGER PRIMARY KEY, toot_id TEXT UNIQUE )", [], ) .unwrap(); migrate_db(d, "tamerelol").unwrap(); let mut stmt = conn.prepare("PRAGMA table_info(tweet_to_toot);").unwrap(); let mut t = stmt.query([]).unwrap(); while let Some(row) = t.next().unwrap() { if row.get::(0).unwrap() == 2 { assert_eq!( row.get::(1).unwrap(), "twitter_screen_name".to_string() ); } } remove_file(d).unwrap(); } #[test] fn test_migrate_db_no_add_column() { let d = "/tmp/test_migrate_db_no_add_column.sqlite"; init_db(d).unwrap(); migrate_db(d, "tamerelol").unwrap(); remove_file(d).unwrap(); } }