Alex Mikhalev
5 years ago
commit
07785918c1
8 changed files with 217 additions and 0 deletions
@ -0,0 +1,15 @@ |
|||||||
|
[package] |
||||||
|
name = "sprinklers_rs" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["Alex Mikhalev <alexmikhalevalex@gmail.com>"] |
||||||
|
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" |
@ -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 |
||||||
|
} |
@ -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(()) |
||||||
|
} |
@ -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<T> = Result<T, MigrationError>; |
||||||
|
|
||||||
|
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<T1: ToString, T2: ToString>(up_sql: T1, down_sql: T2) -> Self { |
||||||
|
Self { |
||||||
|
up_sql: up_sql.to_string(), |
||||||
|
down_sql: down_sql.to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn new_box<T1: ToString, T2: ToString>(up_sql: T1, down_sql: T2) -> Box<dyn Migration> { |
||||||
|
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<MigrationVersion> { |
||||||
|
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<MigrationVersion, Box<dyn Migration>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl Migrations { |
||||||
|
pub fn new() -> Self { |
||||||
|
Self { |
||||||
|
migrations: BTreeMap::new(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn add(&mut self, version: MigrationVersion, migration: Box<dyn Migration>) { |
||||||
|
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(()) |
||||||
|
} |
||||||
|
} |
@ -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<Section, Self::Error> { |
||||||
|
Ok(Section { |
||||||
|
id: row.get(0)?, |
||||||
|
name: row.get(1)?, |
||||||
|
interface_id: row.get(2)?, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
Loading…
Reference in new issue