From 07785918c1cd4096fee490e1bc099c8845f4af25 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Wed, 12 Aug 2020 10:01:44 -0600 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ Cargo.toml | 15 +++++ src/db.rs | 17 ++++++ src/main.rs | 27 +++++++++ src/migrations.rs | 122 +++++++++++++++++++++++++++++++++++++++ src/model/mod.rs | 1 + src/model/section.rs | 21 +++++++ src/section_interface.rs | 10 ++++ 8 files changed, 217 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/db.rs create mode 100644 src/main.rs create mode 100644 src/migrations.rs create mode 100644 src/model/mod.rs create mode 100644 src/model/section.rs create mode 100644 src/section_interface.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d335a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/.vscode +/*.db +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7799c93 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sprinklers_rs" +version = "0.1.0" +authors = ["Alex Mikhalev "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rusqlite = "0.23.1" +log = "0.4.11" +env_logger = "0.7.1" +color-eyre = "0.5.1" +eyre = "0.6.0" +thiserror = "1.0.20" diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..a973e62 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,17 @@ +use crate::migrations::{Migrations, SimpleMigration}; + +pub fn create_migrations() -> Migrations { + let mut migs = Migrations::new(); + migs.add( + 1, + SimpleMigration::new_box( + "CREATE TABLE sections ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + interface_id INTEGER NOT NULL + );", + "DROP TABLE sections;", + ), + ); + migs +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2439bb7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,27 @@ +use rusqlite::NO_PARAMS; +use rusqlite::Connection as DbConnection; +use color_eyre::eyre::Result; + +mod section_interface; +mod model; +mod db; +mod migrations; + +fn setup_db() -> Result<()> { + // let conn = DbConnection::open_in_memory()?; + let mut conn = DbConnection::open("test.db")?; + + let migs = db::create_migrations(); + migs.apply(&mut conn)?; + + Ok(()) +} + +fn main() -> Result<()> { + env_logger::init(); + color_eyre::install()?; + println!("Hello, world!"); + setup_db().unwrap(); + + Ok(()) +} diff --git a/src/migrations.rs b/src/migrations.rs new file mode 100644 index 0000000..c5e650f --- /dev/null +++ b/src/migrations.rs @@ -0,0 +1,122 @@ +use rusqlite::NO_PARAMS; +use rusqlite::{params, Connection}; +use std::collections::BTreeMap; +use std::ops::Bound::{Excluded, Unbounded}; +use thiserror::Error; +use log::debug; + +#[derive(Debug, Error)] +pub enum MigrationError { + #[error("sql error: {0}")] + SqlError(#[from] rusqlite::Error), + #[error("database version {0} too new to migrate")] + VersionTooNew(MigrationVersion), +} + +pub type MigrationResult = Result; + +pub trait Migration { + fn up(&self, conn: &Connection) -> MigrationResult<()>; + fn down(&self, conn: &Connection) -> MigrationResult<()>; +} + +pub struct SimpleMigration { + pub up_sql: String, + pub down_sql: String, +} + +impl SimpleMigration { + pub fn new(up_sql: T1, down_sql: T2) -> Self { + Self { + up_sql: up_sql.to_string(), + down_sql: down_sql.to_string(), + } + } + + pub fn new_box(up_sql: T1, down_sql: T2) -> Box { + Box::new(Self::new(up_sql, down_sql)) + } +} + +impl Migration for SimpleMigration { + fn up(&self, conn: &Connection) -> MigrationResult<()> { + conn.execute(&self.up_sql, NO_PARAMS)?; + Ok(()) + } + fn down(&self, conn: &Connection) -> MigrationResult<()> { + conn.execute(&self.down_sql, NO_PARAMS)?; + Ok(()) + } +} + +pub type MigrationVersion = u32; +pub const NO_MIGRATIONS: MigrationVersion = 0; + +pub fn get_db_version(conn: &Connection) -> MigrationResult { + let table_count: u32 = conn.query_row( + "SELECT COUNT(*) FROM sqlite_master + WHERE type='table' AND name='db_version'", + NO_PARAMS, + |row| row.get(0), + )?; + if table_count == 0 { + return Ok(NO_MIGRATIONS); + } + + let version: u32 = conn.query_row( + "SELECT version FROM db_version WHERE id = 1", + NO_PARAMS, + |row| row.get(0), + )?; + Ok(version) +} + +pub fn set_db_version(conn: &Connection, version: MigrationVersion) -> MigrationResult<()> { + conn.execute( + " + CREATE TABLE IF NOT EXISTS db_version ( + id INTEGER PRIMARY KEY, + version INTEGER + );", NO_PARAMS)?; + conn.execute( + " + INSERT OR REPLACE INTO db_version (id, version) + VALUES (1, ?1);", + params![version])?; + Ok(()) +} + +pub struct Migrations { + migrations: BTreeMap>, +} + +impl Migrations { + pub fn new() -> Self { + Self { + migrations: BTreeMap::new(), + } + } + + pub fn add(&mut self, version: MigrationVersion, migration: Box) { + self.migrations.insert(version, migration); + } + + pub fn apply(&self, conn: &mut Connection) -> MigrationResult<()> { + let db_version = get_db_version(conn)?; + if db_version != 0 && !self.migrations.contains_key(&db_version) { + return Err(MigrationError::VersionTooNew(db_version)); + } + let mig_range = self.migrations.range( + (Excluded(db_version), Unbounded)); + let mut trans = conn.transaction()?; + let mut last_ver: MigrationVersion = 0; + for (ver, mig) in mig_range { + debug!("applying migration version {}", ver); + mig.up(&mut trans)?; + last_ver = *ver; + } + set_db_version(&trans, last_ver)?; + trans.commit()?; + Ok(()) + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..029ef3e --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1 @@ +mod section; diff --git a/src/model/section.rs b/src/model/section.rs new file mode 100644 index 0000000..c2b71b6 --- /dev/null +++ b/src/model/section.rs @@ -0,0 +1,21 @@ +use crate::section_interface::SectionId; +use rusqlite::Row; +use std::convert::TryFrom; + +pub struct Section { + pub id: u32, + pub name: String, + pub interface_id: SectionId, +} + +impl<'a> TryFrom<&Row<'a>> for Section { + type Error = rusqlite::Error; + fn try_from(row: &Row<'a>) -> Result { + Ok(Section { + id: row.get(0)?, + name: row.get(1)?, + interface_id: row.get(2)?, + }) + } +} + diff --git a/src/section_interface.rs b/src/section_interface.rs new file mode 100644 index 0000000..12eb77e --- /dev/null +++ b/src/section_interface.rs @@ -0,0 +1,10 @@ +pub type SectionId = u32; + +pub trait SectionInterface { + fn num_sections() -> SectionId; + fn set_section(id: SectionId, running: bool); + fn get_section(id: SectionId) -> bool; +} + + +