From e86729f23ed8b64dc4b6a1f1681d9c63324da4d3 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Thu, 1 Oct 2020 15:00:15 -0600 Subject: [PATCH] Change how program database update works Have the data sent from the request be applied by the database queries Run queries in a savepoint which can be nested into a transaction --- sprinklers_core/src/model/mod.rs | 4 +- sprinklers_core/src/model/program.rs | 9 ++ sprinklers_database/src/lib.rs | 54 +--------- sprinklers_database/src/program.rs | 143 ++++++++++++++++----------- sprinklers_database/src/sql_json.rs | 39 ++++++++ 5 files changed, 139 insertions(+), 110 deletions(-) create mode 100644 sprinklers_database/src/sql_json.rs diff --git a/sprinklers_core/src/model/mod.rs b/sprinklers_core/src/model/mod.rs index 5c2fcdd..1b36f18 100644 --- a/sprinklers_core/src/model/mod.rs +++ b/sprinklers_core/src/model/mod.rs @@ -3,5 +3,5 @@ mod program; mod section; -pub use program::{Program, ProgramId, ProgramItem, ProgramRef, ProgramSequence, Programs}; -pub use section::{Section, SectionId, SectionRef, Sections}; +pub use program::*; +pub use section::*; diff --git a/sprinklers_core/src/model/program.rs b/sprinklers_core/src/model/program.rs index de4d6ec..44dbd3f 100644 --- a/sprinklers_core/src/model/program.rs +++ b/sprinklers_core/src/model/program.rs @@ -28,3 +28,12 @@ pub struct Program { pub type ProgramRef = Arc; pub type Programs = im::OrdMap; + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct ProgramUpdateData { + pub name: Option, + pub sequence: Option, + pub enabled: Option, + pub schedule: Option, +} diff --git a/sprinklers_database/src/lib.rs b/sprinklers_database/src/lib.rs index 974e2c0..84dd6fb 100644 --- a/sprinklers_database/src/lib.rs +++ b/sprinklers_database/src/lib.rs @@ -2,16 +2,18 @@ mod migration; mod migrations; mod program; mod section; +mod sql_json; pub use migration::*; pub use migrations::create_migrations; +pub use program::*; pub use rusqlite::Connection as DbConn; -use sprinklers_core::model::{Program, Programs, Sections}; +use sprinklers_core::model::Sections; use eyre::Result; -use rusqlite::{params, NO_PARAMS}; +use rusqlite::NO_PARAMS; pub fn setup_db() -> Result { // let conn = DbConn::open_in_memory()?; @@ -39,51 +41,3 @@ pub fn query_sections(conn: &DbConn) -> Result { } Ok(sections) } - -pub fn query_programs(conn: &DbConn) -> Result { - let mut statement = conn.prepare_cached( - " - SELECT p.id, p.name, p.enabled, p.schedule, ps.sequence - FROM programs AS p - INNER JOIN program_sequences AS ps - ON ps.program_id = p.id;", - )?; - let rows = statement.query_map(NO_PARAMS, program::from_sql)?; - let mut programs = Programs::new(); - for row in rows { - let program = row?; - programs.insert(program.id, program.into()); - } - Ok(programs) -} - -pub fn update_program(conn: &mut DbConn, prog: &Program) -> Result<()> { - let trans = conn.transaction()?; - trans - .prepare_cached( - " - UPDATE programs - SET (name, enabled, schedule) = (?2, ?3, ?4) - WHERE id = ?1;", - )? - .execute(&program::as_sql(prog))?; - trans - .prepare_cached( - " - DELETE FROM program_sequence_items AS psi - WHERE psi.program_id = ?1;", - )? - .execute(params![prog.id])?; - let mut insert_prog_seq = trans.prepare_cached( - " - INSERT INTO program_sequence_items - (program_id, seq_num, section_id, duration) - VALUES (?1, ?2, ?3, ?4);", - )?; - for params in program::sequence_as_sql(prog) { - insert_prog_seq.execute(¶ms)?; - } - drop(insert_prog_seq); - trans.commit()?; - Ok(()) -} diff --git a/sprinklers_database/src/program.rs b/sprinklers_database/src/program.rs index 711d51a..bd9cb12 100644 --- a/sprinklers_database/src/program.rs +++ b/sprinklers_database/src/program.rs @@ -1,52 +1,19 @@ +use super::sql_json::SqlJson; +use super::DbConn; use sprinklers_core::{ - model::{Program, ProgramId, ProgramItem, ProgramSequence, SectionId}, + model::{ + Program, ProgramId, ProgramItem, ProgramSequence, ProgramUpdateData, Programs, SectionId, + }, schedule::Schedule, }; -use rusqlite::{ - types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, Value, ValueRef}, - Error as SqlError, Result as SqlResult, Row as SqlRow, ToSql, -}; -use serde::{Deserialize, Serialize}; - -pub struct SqlJson(T); - -impl SqlJson { - pub fn into_inner(self) -> T { - self.0 - } -} - -impl FromSql for SqlJson -where - for<'de> T: Deserialize<'de>, -{ - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - if let ValueRef::Text(text) = value { - let deser_value: T = - serde_json::from_slice(text).map_err(|err| FromSqlError::Other(Box::new(err)))?; - Ok(SqlJson(deser_value)) - } else { - Err(FromSqlError::InvalidType) - } - } -} - -impl ToSql for SqlJson -where - T: Serialize, -{ - fn to_sql(&self) -> SqlResult> { - serde_json::to_string(&self.0) - .map(|serialized| ToSqlOutput::Owned(Value::Text(serialized))) - .map_err(|err| SqlError::ToSqlConversionFailure(Box::new(err))) - } -} +use eyre::Result; +use rusqlite::{params, Row, ToSql, Transaction, NO_PARAMS}; type SqlProgramSequence = SqlJson; type SqlSchedule = SqlJson; -pub fn from_sql<'a>(row: &SqlRow<'a>) -> SqlResult { +fn from_sql<'a>(row: &Row<'a>) -> rusqlite::Result { Ok(Program { id: row.get(0)?, name: row.get(1)?, @@ -56,21 +23,21 @@ pub fn from_sql<'a>(row: &SqlRow<'a>) -> SqlResult { }) } -pub struct SqlProgram<'a> { +struct SqlProgramUpdate<'a> { id: ProgramId, - name: &'a String, - enabled: bool, - schedule: SqlJson<&'a Schedule>, + name: Option<&'a String>, + enabled: Option, + schedule: Option>, } -impl<'a> IntoIterator for &'a SqlProgram<'a> { +impl<'a> IntoIterator for &'a SqlProgramUpdate<'a> { type Item = &'a dyn ToSql; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { vec![ &self.id as &dyn ToSql, - self.name, + &self.name, &self.enabled, &self.schedule, ] @@ -78,16 +45,16 @@ impl<'a> IntoIterator for &'a SqlProgram<'a> { } } -pub fn as_sql(program: &Program) -> SqlProgram { - SqlProgram { - id: program.id, - name: &program.name, +fn update_as_sql(id: ProgramId, program: &ProgramUpdateData) -> SqlProgramUpdate { + SqlProgramUpdate { + id, + name: program.name.as_ref(), enabled: program.enabled, - schedule: SqlJson(&program.schedule), + schedule: program.schedule.as_ref().map(SqlJson), } } -pub struct SqlProgramItem { +struct SqlProgramItem { program_id: ProgramId, seq_num: isize, section_id: SectionId, @@ -109,7 +76,7 @@ impl<'a> IntoIterator for &'a SqlProgramItem { } } -pub fn item_as_sql( +fn item_as_sql( program_item: &ProgramItem, program_id: ProgramId, seq_num: usize, @@ -122,11 +89,71 @@ pub fn item_as_sql( } } -pub fn sequence_as_sql<'a>(program: &'a Program) -> impl Iterator + 'a { - let program_id = program.id; - program - .sequence +fn sequence_as_sql<'a>( + program_id: ProgramId, + sequence: &'a [ProgramItem], +) -> impl Iterator + 'a { + sequence .iter() .enumerate() .map(move |(seq_num, item)| item_as_sql(item, program_id, seq_num)) } + +pub fn query_programs(conn: &DbConn) -> Result { + let query_sql = "\ +SELECT p.id, p.name, p.enabled, p.schedule, ps.sequence +FROM programs AS p + INNER JOIN program_sequences AS ps ON ps.program_id = p.id;"; + let mut statement = conn.prepare_cached(query_sql)?; + let rows = statement.query_map(NO_PARAMS, from_sql)?; + let mut programs = Programs::new(); + for row in rows { + let program = row?; + programs.insert(program.id, program.into()); + } + Ok(programs) +} + +pub fn query_program_by_id(conn: &DbConn, id: ProgramId) -> Result { + let query_sql = "\ +SELECT p.id, p.name, p.enabled, p.schedule, ps.sequence +FROM programs AS p + INNER JOIN program_sequences AS ps ON ps.program_id = p.id +WHERE p.id = ?1;"; + let mut statement = conn.prepare_cached(query_sql)?; + Ok(statement.query_row(params![id], from_sql)?) +} + +pub fn update_program( + trans: &mut Transaction, + id: ProgramId, + prog: &ProgramUpdateData, +) -> Result<()> { + let save = trans.savepoint()?; + let conn = &*save; + let update_sql = "\ +UPDATE programs + SET name = ifnull(?2, name), + enabled = ifnull(?3, enabled), + schedule = ifnull(?4, schedule) + WHERE id = ?1;"; + conn.prepare_cached(update_sql)? + .execute(&update_as_sql(id, prog))?; + if let Some(sequence) = &prog.sequence { + let clear_seq_sql = "\ +DELETE +FROM program_sequence_items +WHERE program_id = ?1;"; + conn.prepare_cached(clear_seq_sql)?.execute(params![id])?; + let insert_seq_sql = "\ +INSERT INTO program_sequence_items (program_id, seq_num, section_id, duration) +VALUES (?1, ?2, ?3, ?4);"; + let mut insert_seq = conn.prepare_cached(insert_seq_sql)?; + for params in sequence_as_sql(id, sequence) { + insert_seq.execute(¶ms)?; + } + drop(insert_seq); + } + save.commit()?; + Ok(()) +} diff --git a/sprinklers_database/src/sql_json.rs b/sprinklers_database/src/sql_json.rs new file mode 100644 index 0000000..0a99831 --- /dev/null +++ b/sprinklers_database/src/sql_json.rs @@ -0,0 +1,39 @@ +use rusqlite::{ + types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, Value, ValueRef}, + ToSql, +}; +use serde::{Deserialize, Serialize}; + +pub struct SqlJson(pub T); + +impl SqlJson { + pub fn into_inner(self) -> T { + self.0 + } +} + +impl FromSql for SqlJson +where + for<'de> T: Deserialize<'de>, +{ + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if let ValueRef::Text(text) = value { + let deser_value: T = + serde_json::from_slice(text).map_err(|err| FromSqlError::Other(Box::new(err)))?; + Ok(SqlJson(deser_value)) + } else { + Err(FromSqlError::InvalidType) + } + } +} + +impl ToSql for SqlJson +where + T: Serialize, +{ + fn to_sql(&self) -> rusqlite::Result> { + serde_json::to_string(&self.0) + .map(|serialized| ToSqlOutput::Owned(Value::Text(serialized))) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err))) + } +}