Browse Source

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
master
Alex Mikhalev 4 years ago
parent
commit
e86729f23e
  1. 4
      sprinklers_core/src/model/mod.rs
  2. 9
      sprinklers_core/src/model/program.rs
  3. 54
      sprinklers_database/src/lib.rs
  4. 143
      sprinklers_database/src/program.rs
  5. 39
      sprinklers_database/src/sql_json.rs

4
sprinklers_core/src/model/mod.rs

@ -3,5 +3,5 @@
mod program; mod program;
mod section; mod section;
pub use program::{Program, ProgramId, ProgramItem, ProgramRef, ProgramSequence, Programs}; pub use program::*;
pub use section::{Section, SectionId, SectionRef, Sections}; pub use section::*;

9
sprinklers_core/src/model/program.rs

@ -28,3 +28,12 @@ pub struct Program {
pub type ProgramRef = Arc<Program>; pub type ProgramRef = Arc<Program>;
pub type Programs = im::OrdMap<ProgramId, ProgramRef>; 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>,
}

54
sprinklers_database/src/lib.rs

@ -2,16 +2,18 @@ mod migration;
mod migrations; mod migrations;
mod program; mod program;
mod section; mod section;
mod sql_json;
pub use migration::*; pub use migration::*;
pub use migrations::create_migrations; pub use migrations::create_migrations;
pub use program::*;
pub use rusqlite::Connection as DbConn; pub use rusqlite::Connection as DbConn;
use sprinklers_core::model::{Program, Programs, Sections}; use sprinklers_core::model::Sections;
use eyre::Result; use eyre::Result;
use rusqlite::{params, NO_PARAMS}; use rusqlite::NO_PARAMS;
pub fn setup_db() -> Result<DbConn> { pub fn setup_db() -> Result<DbConn> {
// let conn = DbConn::open_in_memory()?; // let conn = DbConn::open_in_memory()?;
@ -39,51 +41,3 @@ pub fn query_sections(conn: &DbConn) -> Result<Sections> {
} }
Ok(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(&params)?;
}
drop(insert_prog_seq);
trans.commit()?;
Ok(())
}

143
sprinklers_database/src/program.rs

@ -1,52 +1,19 @@
use super::sql_json::SqlJson;
use super::DbConn;
use sprinklers_core::{ use sprinklers_core::{
model::{Program, ProgramId, ProgramItem, ProgramSequence, SectionId}, model::{
Program, ProgramId, ProgramItem, ProgramSequence, ProgramUpdateData, Programs, SectionId,
},
schedule::Schedule, schedule::Schedule,
}; };
use rusqlite::{ use eyre::Result;
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, Value, ValueRef}, use rusqlite::{params, Row, ToSql, Transaction, NO_PARAMS};
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)))
}
}
type SqlProgramSequence = SqlJson<ProgramSequence>; type SqlProgramSequence = SqlJson<ProgramSequence>;
type SqlSchedule = SqlJson<Schedule>; 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 { Ok(Program {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, 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, id: ProgramId,
name: &'a String, name: Option<&'a String>,
enabled: bool, enabled: Option<bool>,
schedule: SqlJson<&'a Schedule>, 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 Item = &'a dyn ToSql;
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
vec![ vec![
&self.id as &dyn ToSql, &self.id as &dyn ToSql,
self.name, &self.name,
&self.enabled, &self.enabled,
&self.schedule, &self.schedule,
] ]
@ -78,16 +45,16 @@ impl<'a> IntoIterator for &'a SqlProgram<'a> {
} }
} }
pub fn as_sql(program: &Program) -> SqlProgram { fn update_as_sql(id: ProgramId, program: &ProgramUpdateData) -> SqlProgramUpdate {
SqlProgram { SqlProgramUpdate {
id: program.id, id,
name: &program.name, name: program.name.as_ref(),
enabled: program.enabled, enabled: program.enabled,
schedule: SqlJson(&program.schedule), schedule: program.schedule.as_ref().map(SqlJson),
} }
} }
pub struct SqlProgramItem { struct SqlProgramItem {
program_id: ProgramId, program_id: ProgramId,
seq_num: isize, seq_num: isize,
section_id: SectionId, 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_item: &ProgramItem,
program_id: ProgramId, program_id: ProgramId,
seq_num: usize, 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 { fn sequence_as_sql<'a>(
let program_id = program.id; program_id: ProgramId,
program sequence: &'a [ProgramItem],
.sequence ) -> impl Iterator<Item = SqlProgramItem> + 'a {
sequence
.iter() .iter()
.enumerate() .enumerate()
.map(move |(seq_num, item)| item_as_sql(item, program_id, seq_num)) .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(&params)?;
}
drop(insert_seq);
}
save.commit()?;
Ok(())
}

39
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<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…
Cancel
Save