From 0fe30fa7a9ba5870c2dffcb6dc5e18bcf698372f Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Sun, 20 Sep 2020 17:22:56 -0600 Subject: [PATCH] Add support for reading programs from database Closes #8 --- src/db.rs | 3 + src/main.rs | 46 +++++------ src/migrations/0003-table_programs-down.sql | 3 + src/migrations/0003-table_programs-up.sql | 20 +++++ src/migrations/0004-program_rows-down.sql | 2 + src/migrations/0004-program_rows-up.sql | 14 ++++ .../0005-view_program_sequence-down.sql | 1 + .../0005-view_program_sequence-up.sql | 6 ++ src/model/program.rs | 81 ++++++++++++++++++- src/schedule.rs | 2 +- 10 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 src/migrations/0003-table_programs-down.sql create mode 100644 src/migrations/0003-table_programs-up.sql create mode 100644 src/migrations/0004-program_rows-down.sql create mode 100644 src/migrations/0004-program_rows-up.sql create mode 100644 src/migrations/0005-view_program_sequence-down.sql create mode 100644 src/migrations/0005-view_program_sequence-up.sql diff --git a/src/db.rs b/src/db.rs index 0ad1073..4234fda 100644 --- a/src/db.rs +++ b/src/db.rs @@ -14,6 +14,9 @@ pub fn create_migrations() -> Migrations { let mut migs = Migrations::new(); migs.add(include_file_migration!(1, "0001-table_sections")); migs.add(include_file_migration!(2, "0002-section_rows")); + migs.add(include_file_migration!(3, "0003-table_programs")); + migs.add(include_file_migration!(4, "0004-program_rows")); + migs.add(include_file_migration!(5, "0005-view_program_sequence")); // INSERT MIGRATION ABOVE -- DO NOT EDIT THIS COMMENT migs } diff --git a/src/main.rs b/src/main.rs index 3af5dfd..6ed0769 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,10 +18,8 @@ mod section_runner; #[cfg(test)] mod trace_listeners; -use im::ordmap; -use model::{Program, ProgramItem, ProgramRef, Section, Sections}; -use schedule::Schedule; -use std::{sync::Arc, time::Duration}; +use model::{Program, Programs, Section, Sections}; +use std::sync::Arc; fn setup_db() -> Result { // let conn = DbConnection::open_in_memory()?; @@ -47,6 +45,23 @@ fn query_sections(conn: &DbConnection) -> Result { Ok(sections) } +fn query_programs(conn: &DbConnection) -> Result { + let mut statement = conn.prepare_cached( + " + SELECT p.id, p.name, ps.sequence, p.enabled, p.schedule + 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) +} + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() @@ -69,28 +84,7 @@ async fn main() -> Result<()> { let mut section_runner = section_runner::SectionRunner::new(section_interface); let mut program_runner = program_runner::ProgramRunner::new(section_runner.clone()); - let run_time = chrono::Local::now().time() + chrono::Duration::seconds(5); - let schedule = Schedule::new( - vec![run_time], - schedule::every_day(), - schedule::DateTimeBound::None, - schedule::DateTimeBound::None, - ); - let program: ProgramRef = Program { - id: 1, - name: "Test Program".into(), - sequence: sections - .values() - .map(|sec| ProgramItem { - section_id: sec.id, - duration: Duration::from_secs(2), - }) - .collect(), - enabled: true, - schedule, - } - .into(); - let programs = ordmap![1 => program]; + let programs = query_programs(&conn)?; program_runner.update_sections(sections.clone()).await?; program_runner.update_programs(programs.clone()).await?; diff --git a/src/migrations/0003-table_programs-down.sql b/src/migrations/0003-table_programs-down.sql new file mode 100644 index 0000000..dd89e29 --- /dev/null +++ b/src/migrations/0003-table_programs-down.sql @@ -0,0 +1,3 @@ +DROP INDEX program_sequence_items_idx1; +DROP TABLE program_sequence_items; +DROP TABLE programs; \ No newline at end of file diff --git a/src/migrations/0003-table_programs-up.sql b/src/migrations/0003-table_programs-up.sql new file mode 100644 index 0000000..e765af3 --- /dev/null +++ b/src/migrations/0003-table_programs-up.sql @@ -0,0 +1,20 @@ +CREATE TABLE programs +( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + schedule TEXT NOT NULL +); + +CREATE TABLE program_sequence_items +( + seq_num INTEGER NOT NULL, + program_id INTEGER NOT NULL, + section_id INTEGER NOT NULL, + duration REAL NOT NULL, + FOREIGN KEY (program_id) REFERENCES programs (id), + FOREIGN KEY (section_id) REFERENCES sections (id) +); + +CREATE UNIQUE INDEX program_sequence_items_idx1 + ON program_sequence_items (program_id, seq_num); \ No newline at end of file diff --git a/src/migrations/0004-program_rows-down.sql b/src/migrations/0004-program_rows-down.sql new file mode 100644 index 0000000..e105022 --- /dev/null +++ b/src/migrations/0004-program_rows-down.sql @@ -0,0 +1,2 @@ +DELETE FROM program_sequence_items; +DELETE FROM programs; \ No newline at end of file diff --git a/src/migrations/0004-program_rows-up.sql b/src/migrations/0004-program_rows-up.sql new file mode 100644 index 0000000..b9c69d2 --- /dev/null +++ b/src/migrations/0004-program_rows-up.sql @@ -0,0 +1,14 @@ +INSERT INTO programs (id, name, enabled, schedule) +VALUES (1, 'Test Program', TRUE, + json_object( + 'times', json('[{"hour": 16, "minute": 1, "second": 0}]'), + 'weekdays', json_array(0, 1, 2, 3, 4, 5, 6), + 'from', NULL, + 'to', NULL)); + +INSERT INTO program_sequence_items (seq_num, program_id, section_id, duration) +SELECT row_number() OVER (ORDER BY s.id) seq_num, + (SELECT p.id FROM programs as p WHERE p.name = 'Test Program') program_id, + s.id section_id, + 2.0 duration +FROM sections AS s; \ No newline at end of file diff --git a/src/migrations/0005-view_program_sequence-down.sql b/src/migrations/0005-view_program_sequence-down.sql new file mode 100644 index 0000000..c62b1ae --- /dev/null +++ b/src/migrations/0005-view_program_sequence-down.sql @@ -0,0 +1 @@ +DROP VIEW program_sequences; diff --git a/src/migrations/0005-view_program_sequence-up.sql b/src/migrations/0005-view_program_sequence-up.sql new file mode 100644 index 0000000..bca15c7 --- /dev/null +++ b/src/migrations/0005-view_program_sequence-up.sql @@ -0,0 +1,6 @@ +CREATE VIEW program_sequences AS +SELECT psi.program_id program_id, + json_group_array(json_object( + 'section_id', psi.section_id, + 'duration', psi.duration)) sequence +FROM program_sequence_items as psi; diff --git a/src/model/program.rs b/src/model/program.rs index 9a13a77..0e47658 100644 --- a/src/model/program.rs +++ b/src/model/program.rs @@ -1,11 +1,36 @@ -use std::{time::Duration, sync::Arc}; use super::section::SectionId; use crate::schedule::Schedule; +use serde::{Deserialize, Serialize}; +use std::{sync::Arc, time::Duration}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProgramItem { - pub section_id: SectionId, - pub duration: Duration, + pub section_id: SectionId, + #[serde( + serialize_with = "ser::serialize_duration", + deserialize_with = "ser::deserialize_duration" + )] + pub duration: Duration, +} + +mod ser { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::time::Duration; + + pub fn serialize_duration(duration: &Duration, serializer: S) -> Result + where + S: Serializer, + { + duration.as_secs_f64().serialize(serializer) + } + + pub fn deserialize_duration<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let secs: f64 = Deserialize::deserialize(deserializer)?; + Ok(Duration::from_secs_f64(secs)) + } } pub type ProgramSequence = Vec; @@ -21,6 +46,54 @@ pub struct Program { pub schedule: Schedule, } +mod sql { + use super::{Program, ProgramSequence}; + use crate::schedule::Schedule; + use rusqlite::{ + types::{FromSql, FromSqlError, FromSqlResult, ValueRef}, + Result as SqlResult, Row as SqlRow, + }; + use serde::Deserialize; + + struct SqlJson(T); + + impl SqlJson { + 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) + } + } + } + + type SqlProgramSequence = SqlJson; + type SqlSchedule = SqlJson; + + impl Program { + pub fn from_sql<'a>(row: &SqlRow<'a>) -> SqlResult { + Ok(Self { + id: row.get(0)?, + name: row.get(1)?, + sequence: row.get::<_, SqlProgramSequence>(2)?.into_inner(), + enabled: row.get(3)?, + schedule: row.get::<_, SqlSchedule>(4)?.into_inner(), + }) + } + } +} + pub type ProgramRef = Arc; pub type Programs = im::OrdMap; diff --git a/src/schedule.rs b/src/schedule.rs index 9cdbd9a..38d254d 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -188,7 +188,7 @@ mod ser { impl fmt::Display for InvalidWeekday { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - "invalid weekday".fmt(f) + "weekday out of range 0 to 6".fmt(f) } }