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 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::*;
|
||||||
|
@ -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>,
|
||||||
|
}
|
||||||
|
@ -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(¶ms)?;
|
|
||||||
}
|
|
||||||
drop(insert_prog_seq);
|
|
||||||
trans.commit()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -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(¶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