diff --git a/Cargo.lock b/Cargo.lock index 26c7a85..891e05b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "cc" version = "1.0.76" @@ -100,6 +109,35 @@ dependencies = [ "winapi", ] +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -112,6 +150,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -214,6 +262,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "sha2", "simple_logger", ] @@ -337,6 +386,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "simple_logger" version = "4.0.0" @@ -411,6 +471,12 @@ dependencies = [ "time-core", ] +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + [[package]] name = "unicode-ident" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 5239c9b..fc089c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ clap-verbosity-flag = "2.0.0" simple_logger = "4.0.0" log = "0.4.17" home = "0.5.4" +sha2 = "0.10.6" serde_json = "1.0" \ No newline at end of file diff --git a/flake.nix b/flake.nix index 4ad9ad8..31d7637 100644 --- a/flake.nix +++ b/flake.nix @@ -23,11 +23,9 @@ name = "IntelliJ development environment"; paths = [ # something that work.. ish - # pre-commit - # rustup - - - rustc cargo rustPlatform.rustcSrc gcc + rustc + cargo + rustPlatform.rustcSrc gcc ]; }; diff --git a/src/main.rs b/src/main.rs index 464c6bd..5f9a63a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use crate::migration::migrate; mod migration; mod users; +mod securities; #[derive(Debug, Serialize)] struct Output { @@ -31,7 +32,7 @@ trait CommandLineUserLogic { fn list_user(&mut self, conn: &Connection); - // fn list_securities(&mut self, conn: &Connection); + fn list_securities(&mut self, conn: &Connection); } fn verbosity_arg() -> Arg { @@ -84,11 +85,23 @@ fn user_command_line_parser() -> Command { .subcommand(ls_user_command_line_parser()); } +fn ls_securities_command_line_parser() -> Command { + return Command::new("ls") + .long_flag("ls") + .about("List all securities."); +} + +fn securities_command_line_parser() -> Command { + return Command::new("securities") + .subcommand(ls_securities_command_line_parser()); +} + fn command_line_parser() -> Command { return Command::new("prog") .arg(verbosity_arg()) .arg(db_path_arg()) - .subcommand(user_command_line_parser()); + .subcommand(user_command_line_parser()) + .subcommand(securities_command_line_parser()); } fn process_new_user_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { @@ -116,12 +129,33 @@ fn process_user_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn } } + +fn process_list_securities_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { + debug!("Processing list securities command line {:?}.", m); + logic.list_securities(conn); +} + + +fn process_securities_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { + debug!("Processing securities command line {:?}.", m); + match m.subcommand() { + Some(("ls", list_securities_match)) => { + process_list_securities_command_line(conn, list_securities_match, logic); + } + _ => unreachable!(), + } +} + fn process_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { debug!("Processing command line {:?}.", m); match m.subcommand() { Some(("users", user_match)) => { process_user_command_line(conn, user_match, logic); } + + Some(("securities", securities_match)) => { + process_securities_command_line(conn, securities_match, logic); + } _ => { println!("Received unparsable command. {:?}", m.subcommand()); unreachable!(); @@ -136,7 +170,7 @@ mod test { struct TestCommandLineLogic { list_user_count: usize, create_user_count: usize, - + list_securities_count: usize, } impl TestCommandLineLogic { @@ -144,6 +178,7 @@ mod test { return TestCommandLineLogic { list_user_count: 0, create_user_count: 0, + list_securities_count: 0, }; } } @@ -156,6 +191,10 @@ mod test { fn list_user(&mut self, conn: &Connection) { self.list_user_count += 1; } + + fn list_securities(&mut self, conn: &Connection) { + self.list_securities_count += 1; + } } #[test] @@ -210,6 +249,16 @@ mod test { process_command_line(&conn, &m, &mut logic); assert_eq!(logic.list_user_count, 1); } + + #[test] + fn list_securities() { + let cmd = command_line_parser(); + let m = cmd.get_matches_from(vec!["test", "securities", "ls"]); + let mut logic = TestCommandLineLogic::new(); + let conn = Connection::open_in_memory().unwrap(); + process_command_line(&conn, &m, &mut logic); + assert_eq!(logic.list_securities_count, 1); + } } struct ProCommandLineLogic {} @@ -243,6 +292,10 @@ impl CommandLineUserLogic for ProCommandLineLogic { } } } + + fn list_securities(&mut self, conn: &Connection) { + todo!() + } } @@ -262,7 +315,7 @@ fn main() { set_logger(&m); let conn = get_connection(&m); - migrate(&conn); + // migrate(&conn); let mut logic = ProCommandLineLogic::new(); process_command_line(&conn, &m, &mut logic); } \ No newline at end of file diff --git a/src/migration.rs b/src/migration.rs index bfc133c..0596975 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -1,4 +1,13 @@ -use rusqlite::Connection; +use rusqlite::{Connection, Transaction}; + +use sha2::{Digest, Sha256, Sha512}; + +const INITIAL_MIGRATION: &str = " +CREATE TABLE IF NOT EXISTS shitty_migrations ( + id INTEGER PRIMARY KEY, + sha256 BLOB(2) +); +"; const MIGRATION_SCRIPT: &str = " @@ -20,24 +29,113 @@ CREATE TABLE IF NOT EXISTS securities ( symbol TEXT NOT NULL UNIQUE, FOREIGN KEY(security_type) REFERENCES security_types(id) - ); - -CREATE TABLE IF NOT EXISTS account( - id INTEGER PRIMARY KEY AUTOINCREMENT, - security INTEGER NOT NULL, - owner INTEGER NOT NULL, - - FOREIGN KEY(security) REFERENCES securities(id) -); - -CREATE TABLE IF NOT EXISTS account_values( - id INTEGER PRIMARY KEY AUTOINCREMENT -); - +// +// CREATE TABLE IF NOT EXISTS account( +// id INTEGER PRIMARY KEY AUTOINCREMENT, +// security INTEGER NOT NULL, +// owner INTEGER NOT NULL, +// +// FOREIGN KEY(security) REFERENCES securities(id) +// ); +// +// CREATE TABLE IF NOT EXISTS account_values( +// id INTEGER PRIMARY KEY AUTOINCREMENT +// ); "; -pub fn migrate(conn: & Connection) { - conn.execute(MIGRATION_SCRIPT, ()).unwrap(); +fn get_id_of_new_migration(tx: &Transaction) -> usize { + let q = "SELECT COALESCE(MAX(id) + 1, 0) FROM shitty_migrations"; + return tx.query_row(q, (), |row| row.get::(0)).unwrap(); +} + + +pub fn migrate(conn: &mut Connection, migrations: &Vec) { + let tx = conn.transaction().unwrap(); + tx.execute(INITIAL_MIGRATION, ()).unwrap(); + let id = get_id_of_new_migration(&tx); + + for (i, migration) in migrations.iter().enumerate() { + if i < id { + check_migration(&tx, i, migration); + } else { + apply_migration(&tx, i, migration); + } + } + tx.commit().unwrap(); +} + +fn apply_migration(tx: &Transaction, id: usize, migration: &String) { + let hash = compute_migration_sha256(migration); + tx.execute(migration, ()).unwrap(); + let q = "INSERT INTO shitty_migrations(id, sha256) VALUES (?1, ?2)"; + tx.execute(q, (&id, &hash)).unwrap(); +} + +fn compute_migration_sha256(migration: &String) -> String { + let mut hasher = Sha256::new(); + hasher.update(migration); + let ret = hasher.finalize(); + return format!("{:x}", ret); +} + +fn check_migration(tx: &Transaction, id: usize, migration: &String) -> Result { + let q = "SELECT sha256 FROM shitty_migrations WHERE id = ?1"; + let res = tx.query_row(q, (&id, ), |row| row.get::(0)).unwrap(); + let expected = compute_migration_sha256(migration); + assert_eq!(&res, &expected); +} + +#[cfg(test)] +mod test { + use super::*; + + fn create_basic_migrations() -> Vec { + let migration1 = "CREATE TABLE people(name TEXT, age INTEGER);".to_string(); + let migration2 = "INSERT INTO people(name, age) VALUES (\"didier\", 37);".to_string(); + return vec![migration1, migration2]; + } + + fn create_basic_migrations_2() -> Vec { + let migration1 = "CREATE TABLE people(name TEXT, age INTEGER);".to_string(); + let migration2 = "INSERT INTO people(name, age) VALUES (\"stephanie\", 38);".to_string(); + return vec![migration1, migration2]; + } + + #[test] + fn basic() { + let mut conn = Connection::open_in_memory().unwrap(); + migrate(&mut conn, &create_basic_migrations()); + let tx = conn.transaction().unwrap(); + assert_eq!(get_id_of_new_migration(&tx), 2); + + let q = "SELECT COUNT(*) FROM people;"; + let c = tx.query_row(q, (), |x| x.get::(0)).unwrap(); + assert_eq!(c, c); + } + + #[test] + fn idempotency() { + let mut conn = Connection::open_in_memory().unwrap(); + + migrate(&mut conn, &create_basic_migrations()); + migrate(&mut conn, &create_basic_migrations()); + + let tx = conn.transaction().unwrap(); + + assert_eq!(get_id_of_new_migration(&tx), 2); + let q = "SELECT COUNT(*) FROM people;"; + let c = tx.query_row(q, (), |x| x.get::(0)).unwrap(); + assert_eq!(c, c); + } + + #[test] + fn broken_migration() { + let mut conn = Connection::open_in_memory().unwrap(); + + migrate(&mut conn, &create_basic_migrations()); + migrate(&mut conn, &create_basic_migrations_2()); + + } } \ No newline at end of file diff --git a/src/securities.rs b/src/securities.rs new file mode 100644 index 0000000..139597f --- /dev/null +++ b/src/securities.rs @@ -0,0 +1,2 @@ + +