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
This commit is contained in:
		
							parent
							
								
									0e45ab5aa9
								
							
						
					
					
						commit
						e86729f23e
					
				| @ -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::*; | ||||
|  | ||||
| @ -28,3 +28,12 @@ pub struct Program { | ||||
| pub type ProgramRef = Arc<Program>; | ||||
| 
 | ||||
| pub type Programs = im::OrdMap<ProgramId, ProgramRef>; | ||||
| 
 | ||||
| #[derive(Default, Debug, Serialize, Deserialize)] | ||||
| #[serde(default, rename_all = "camelCase")] | ||||
| pub struct ProgramUpdateData { | ||||
|     pub name: Option<String>, | ||||
|     pub sequence: Option<ProgramSequence>, | ||||
|     pub enabled: Option<bool>, | ||||
|     pub schedule: Option<Schedule>, | ||||
| } | ||||
|  | ||||
| @ -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<DbConn> { | ||||
|     // let conn = DbConn::open_in_memory()?;
 | ||||
| @ -39,51 +41,3 @@ pub fn query_sections(conn: &DbConn) -> Result<Sections> { | ||||
|     } | ||||
|     Ok(sections) | ||||
| } | ||||
| 
 | ||||
| pub fn query_programs(conn: &DbConn) -> Result<Programs> { | ||||
|     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(()) | ||||
| } | ||||
|  | ||||
| @ -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>(T); | ||||
| 
 | ||||
| impl<T> SqlJson<T> { | ||||
|     pub fn into_inner(self) -> T { | ||||
|         self.0 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> FromSql for SqlJson<T> | ||||
| where | ||||
|     for<'de> T: Deserialize<'de>, | ||||
| { | ||||
|     fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { | ||||
|         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<T> ToSql for SqlJson<T> | ||||
| where | ||||
|     T: Serialize, | ||||
| { | ||||
|     fn to_sql(&self) -> SqlResult<ToSqlOutput<'_>> { | ||||
|         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<ProgramSequence>; | ||||
| type SqlSchedule = SqlJson<Schedule>; | ||||
| 
 | ||||
| pub fn from_sql<'a>(row: &SqlRow<'a>) -> SqlResult<Program> { | ||||
| fn from_sql<'a>(row: &Row<'a>) -> rusqlite::Result<Program> { | ||||
|     Ok(Program { | ||||
|         id: row.get(0)?, | ||||
|         name: row.get(1)?, | ||||
| @ -56,21 +23,21 @@ pub fn from_sql<'a>(row: &SqlRow<'a>) -> SqlResult<Program> { | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| pub struct SqlProgram<'a> { | ||||
| struct SqlProgramUpdate<'a> { | ||||
|     id: ProgramId, | ||||
|     name: &'a String, | ||||
|     enabled: bool, | ||||
|     schedule: SqlJson<&'a Schedule>, | ||||
|     name: Option<&'a String>, | ||||
|     enabled: Option<bool>, | ||||
|     schedule: Option<SqlJson<&'a Schedule>>, | ||||
| } | ||||
| 
 | ||||
| impl<'a> IntoIterator for &'a SqlProgram<'a> { | ||||
| impl<'a> IntoIterator for &'a SqlProgramUpdate<'a> { | ||||
|     type Item = &'a dyn ToSql; | ||||
|     type IntoIter = std::vec::IntoIter<Self::Item>; | ||||
| 
 | ||||
|     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<Item = SqlProgramItem> + 'a { | ||||
|     let program_id = program.id; | ||||
|     program | ||||
|         .sequence | ||||
| fn sequence_as_sql<'a>( | ||||
|     program_id: ProgramId, | ||||
|     sequence: &'a [ProgramItem], | ||||
| ) -> impl Iterator<Item = SqlProgramItem> + 'a { | ||||
|     sequence | ||||
|         .iter() | ||||
|         .enumerate() | ||||
|         .map(move |(seq_num, item)| item_as_sql(item, program_id, seq_num)) | ||||
| } | ||||
| 
 | ||||
| pub fn query_programs(conn: &DbConn) -> Result<Programs> { | ||||
|     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<Program> { | ||||
|     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(()) | ||||
| } | ||||
|  | ||||
							
								
								
									
										39
									
								
								sprinklers_database/src/sql_json.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								sprinklers_database/src/sql_json.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| use rusqlite::{ | ||||
|     types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, Value, ValueRef}, | ||||
|     ToSql, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| pub struct SqlJson<T>(pub T); | ||||
| 
 | ||||
| impl<T> SqlJson<T> { | ||||
|     pub fn into_inner(self) -> T { | ||||
|         self.0 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T> FromSql for SqlJson<T> | ||||
| where | ||||
|     for<'de> T: Deserialize<'de>, | ||||
| { | ||||
|     fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> { | ||||
|         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<T> ToSql for SqlJson<T> | ||||
| where | ||||
|     T: Serialize, | ||||
| { | ||||
|     fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> { | ||||
|         serde_json::to_string(&self.0) | ||||
|             .map(|serialized| ToSqlOutput::Owned(Value::Text(serialized))) | ||||
|             .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err))) | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user