aquarium_control/database/database_interface_feed_trait.rs
1/* Copyright 2025 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10//! Defines the database interaction contract for the `feed` module.
11//!
12//! This module is central to decoupling the application's feeding logic from the
13//! underlying database implementation. It achieves this by defining the
14//! `DatabaseInterfaceFeedTrait`, which specifies all the database operations
15//! that the `feed` module requires.
16//!
17//! ## Key Components
18//!
19//! - **`DatabaseInterfaceFeedTrait`**: An abstraction layer that defines a set of methods
20//! for querying and modifying feed-related data, such as feed schedules and
21//! historical feed events. By depending on this trait instead of a concrete
22//! database struct, the `feed` module can be tested in isolation using mock
23//! implementations.
24//!
25//! - **`impl DatabaseInterfaceFeedTrait for SqlInterfaceFeed`**: The production
26//! implementation of the trait. This block contains the actual SQL queries and
27//! logic required to interact with an SQL database, translating the high-level
28//! trait methods into concrete database operations.
29//!
30//! ## Design and Purpose
31//!
32//! The primary goal of this module is to enable **testability and modularity**.
33//! The `feed` thread's logic is complex, and testing it against a live database
34//! is slow, brittle, and requires significant setup. By using a trait object,
35//! we can inject a mock database during testing.
36
37#[cfg(test)]
38use log::error;
39
40use crate::database::pingable::Pingable;
41use crate::database::sql_interface::SqlInterface;
42use crate::database::sql_interface_error::SqlInterfaceError;
43use crate::database::sql_interface_feed::SqlInterfaceFeed;
44use crate::database::sql_query_strings;
45use crate::food::feed_pattern::{FeedPhase, Feedpattern};
46use crate::food::feed_schedule_entry::FeedScheduleEntry;
47use chrono::NaiveDateTime;
48use mysql::params;
49use mysql::prelude::Queryable;
50
51pub const NUM_FEEDPHASES: i32 = 10;
52
53/// Trait for interfacing between feed and SQL database
54/// This trait is prerequisite to use mock implementation for testing
55pub trait DatabaseInterfaceFeedTrait: Pingable {
56 fn get_current_timestamp(&mut self) -> mysql::Result<NaiveDateTime, SqlInterfaceError>;
57
58 fn get_past_feedschedule_entries_from_database(
59 &mut self,
60 ) -> mysql::Result<Option<Vec<FeedScheduleEntry>>, SqlInterfaceError>;
61
62 fn insert_feed_event(
63 &mut self,
64 timestamp: NaiveDateTime,
65 feeder_run_time: f64,
66 profile_name: String,
67 profile_id: i32,
68 ) -> mysql::Result<(), SqlInterfaceError>;
69
70 fn get_single_feedpattern_from_database(
71 &mut self,
72 profile_id: i32,
73 ) -> mysql::Result<Feedpattern, SqlInterfaceError>;
74
75 fn update_feedschedule_entries_in_database(
76 &mut self,
77 feedschedule_entries: &mut Vec<FeedScheduleEntry>,
78 ) -> mysql::Result<(), SqlInterfaceError>;
79
80 #[cfg(test)]
81 fn insert_feed_schedule_entry(
82 &mut self,
83 timestamp: NaiveDateTime,
84 profile_id: i32,
85 profile_name: String,
86 is_weekly: bool,
87 is_daily: bool,
88 ) -> mysql::Result<(), SqlInterfaceError>;
89}
90
91impl DatabaseInterfaceFeedTrait for SqlInterfaceFeed {
92 /// Retrieves the current timestamp from the database using the internal connection.
93 ///
94 /// This function acts as a proxy, calling the general `SqlInterface::get_current_timestamp`
95 /// method with the `SqlInterfaceFeed`'s established database connection.
96 ///
97 /// # Returns
98 /// A `Result` which is:
99 /// - `Ok(NaiveDateTime)`: The current timestamp as reported by the database.
100 /// - `Err(SqlInterfaceError)`: If any error occurs during the retrieval or parsing
101 /// of the timestamp from the database.
102 fn get_current_timestamp(&mut self) -> mysql::Result<NaiveDateTime, SqlInterfaceError> {
103 SqlInterface::get_current_timestamp(&mut self.conn)
104 }
105
106 /// Retrieves feed schedule entries from the database that are scheduled in the past.
107 ///
108 /// This function queries the database for all feed schedule entries whose timestamps
109 /// are before the current time. The raw database entries are then post-processed
110 /// into `FeedScheduleEntry` structs.
111 ///
112 /// # Returns
113 /// A `Result` which is:
114 /// - `Ok(Some(Vec<FeedScheduleEntry>))` if one or more past feed schedule entries are found.
115 /// - `Ok(None)` if no past feed schedule entries are found in the database.
116 /// - `Err(SqlInterfaceError)` if any error occurs during the database query
117 /// or during the conversion of raw database data into `FeedScheduleEntry` structs.
118 fn get_past_feedschedule_entries_from_database(
119 &mut self,
120 ) -> mysql::Result<Option<Vec<FeedScheduleEntry>>, SqlInterfaceError> {
121 self.get_feedschedule_entries_from_database(
122 sql_query_strings::SQL_QUERY_READ_PAST_FEED_SCHEDULE_ENTRY,
123 )
124 }
125
126 /// Inserts a new feed event record into the database.
127 ///
128 /// This function logs details about a completed feed operation, including the
129 /// timestamp, how long the feeder ran, and which feed profile was used.
130 ///
131 /// # Arguments
132 /// * `timestamp` - The `NaiveDateTime` when the feed event occurred.
133 /// * `feeder_run_time` - The duration (in seconds) the feeder motor was active.
134 /// * `profile_name` - The name of the feed profile that was executed for this event.
135 /// * `profile_id` - The unique identifier of the feed profile that was executed.
136 ///
137 /// # Returns
138 /// An empty `Result` (`Ok(())`) if the feed event record was successfully inserted.
139 ///
140 /// # Errors
141 /// Returns `SqlInterfaceError::InsertFeedEventFailure` if the `INSERT` query fails.
142 /// This can happen due to a lost connection, constraint violation (e.g., foreign key),
143 /// or incorrect data types. The original `mysql::Error` is included as the source.
144 fn insert_feed_event(
145 &mut self,
146 timestamp: NaiveDateTime,
147 feeder_run_time: f64,
148 profile_name: String,
149 profile_id: i32,
150 ) -> mysql::Result<(), SqlInterfaceError> {
151 self.conn
152 .exec_drop::<_, _>(
153 sql_query_strings::SQL_QUERY_WRITE_FEED_EVENT.to_owned(),
154 params! {
155 "timestamp" => timestamp,
156 "feeder_run_time" => feeder_run_time,
157 "profile_name" => profile_name,
158 "profile_id" => profile_id,
159 },
160 )
161 .map_err(|e| SqlInterfaceError::InsertFeedEventFailure {
162 location: module_path!().to_string(),
163 source: e,
164 })?;
165
166 Ok(())
167 }
168
169 /// Retrieves a complete `Feedpattern` from the SQL database, including its header and all associated phases.
170 ///
171 /// This function first fetches the header information for the specified `profile_id`.
172 /// It then retrieves data for each of the 10 expected feed phases, converting raw
173 /// database flags into booleans.
174 ///
175 /// # Arguments
176 /// * `profile_id` - The unique identifier of the feed profile to retrieve.
177 ///
178 /// # Returns
179 /// A `Result` containing a fully constructed `Feedpattern` object on success.
180 ///
181 /// # Errors
182 /// This function will return an error if:
183 /// - The query for the feed pattern header fails, the header is not found, or multiple headers are found.
184 /// - The query for any of the 10 feed phases fails, a phase is not found, or multiple phases are found.
185 /// - A retrieved feed phase contains an invalid (negative) duration value.
186 fn get_single_feedpattern_from_database(
187 &mut self,
188 profile_id: i32,
189 ) -> mysql::Result<Feedpattern, SqlInterfaceError> {
190 let sql_feedpattern_header = self.get_feedpattern_header_from_database(profile_id)?;
191
192 let mut feed_phases = Vec::new();
193 for nr in 1..=NUM_FEEDPHASES {
194 let sql_feed_phase = self.get_feed_phase_from_database(profile_id, nr)?;
195
196 let feed_phase = FeedPhase {
197 pause_duration: sql_feed_phase.pause_duration,
198 pause_skimmer: sql_feed_phase.pause_skimmer > 0,
199 pause_main_pump_1: sql_feed_phase.pause_main_pump_1 > 0,
200 pause_main_pump_2: sql_feed_phase.pause_main_pump_2 > 0,
201 pause_aux_pump_1: sql_feed_phase.pause_aux_pump_1 > 0,
202 pause_aux_pump_2: sql_feed_phase.pause_aux_pump_2 > 0,
203 feed_duration: sql_feed_phase.feed_duration,
204 feed_skimmer: sql_feed_phase.feed_skimmer > 0,
205 feed_main_pump_1: sql_feed_phase.feed_main_pump_1 > 0,
206 feed_main_pump_2: sql_feed_phase.feed_main_pump_2 > 0,
207 feed_aux_pump_1: sql_feed_phase.feed_aux_pump_1 > 0,
208 feed_aux_pump_2: sql_feed_phase.feed_aux_pump_2 > 0,
209 };
210 feed_phases.push(feed_phase);
211 }
212 Ok(Feedpattern {
213 profile_id: sql_feedpattern_header.profile_id,
214 profile_name: sql_feedpattern_header.profile_name,
215 feedphases: feed_phases,
216 })
217 }
218
219 /// Updates or deletes multiple feed schedule entries in the database.
220 ///
221 /// This function iterates through a list of `FeedScheduleEntry` structs. For each entry:
222 /// - If `repeat_daily` is `true`, it updates the entry's timestamp in the database to tomorrow's date at the same time.
223 /// - If `repeat_daily` is `false`, it deletes the entry from the database.
224 ///
225 /// The entire operation is transactional in nature: it will stop and return an error on the first failure.
226 ///
227 /// # Arguments
228 /// * `feedschedule_entries` - A mutable reference to a vector of `FeedScheduleEntry` structs to be processed.
229 ///
230 /// # Returns
231 /// An empty `Result` (`Ok(())`) if all update and delete operations were successful.
232 ///
233 /// # Errors
234 /// This function will return an error if an operation fails. This can be:
235 /// - `FeedScheduleUpdateFailure`: If fetching tomorrow's date fails.
236 /// - `FeedScheduleDropFailure`: If a database `UPDATE` or `DELETE` command fails. This can be due to a lost connection or other database-side issues.
237 fn update_feedschedule_entries_in_database(
238 &mut self,
239 feedschedule_entries: &mut Vec<FeedScheduleEntry>,
240 ) -> mysql::Result<(), SqlInterfaceError> {
241 let mut result = Ok(()); // an initial value - will be overwritten by any error occurred
242 for feedschedule_entry in feedschedule_entries {
243 if feedschedule_entry.repeat_daily {
244 // the feed schedule shall be repeated daily, so update the entry with
245 // - tomorrow's date
246 // - the same time of day as the entry
247 let sql_query_string =
248 sql_query_strings::SQL_QUERY_UPDATE_FEED_SCHEDULE_ENTRY.to_owned();
249
250 let timestamp_tomorrow_date_only =
251 match SqlInterface::get_tomorrow_date(&mut self.conn) {
252 Ok(c) => c,
253 Err(e) => {
254 result = Err(SqlInterfaceError::FeedScheduleUpdateFailure {
255 location: module_path!().to_string(),
256 source: Box::new(e),
257 });
258 continue;
259 }
260 };
261
262 let timestamp_tomorrow = NaiveDateTime::new(
263 timestamp_tomorrow_date_only,
264 feedschedule_entry.timestamp.time(),
265 );
266 match self.conn.exec_drop::<_, _>(
267 sql_query_string,
268 params! {
269 "timestamp_new" => timestamp_tomorrow,
270 "timestamp_old" => feedschedule_entry.timestamp,
271 },
272 ) {
273 Ok(_) => { /* do nothing */ }
274 Err(e) => {
275 result = Err(SqlInterfaceError::FeedScheduleDropFailure {
276 location: module_path!().to_string(),
277 source: e,
278 });
279 }
280 };
281 } else {
282 // feed schedule shall not be repeated daily, delete the entry from the database
283 let sql_query_string =
284 sql_query_strings::SQL_QUERY_DELETE_FEED_SCHEDULE_ENTRY.to_owned();
285 match self.conn.exec_drop::<_, _>(
286 sql_query_string,
287 params! {
288 "timestamp" => feedschedule_entry.timestamp,
289 },
290 ) {
291 Ok(_) => { /* do nothing */ }
292 Err(e) => {
293 result = Err(SqlInterfaceError::FeedScheduleDropFailure {
294 location: module_path!().to_string(),
295 source: e,
296 });
297 }
298 };
299 }
300 }
301 result
302 }
303
304 #[cfg(test)]
305 // Inserts a single feed schedule entry into the database.
306 //
307 // This helper function is exclusively used in test environments to
308 // programmatically set up specific feed schedule configurations.
309 //
310 // # Arguments
311 // * `timestamp` - The `NaiveDateTime` when the feed event is scheduled to occur.
312 // * `profile_id` - The unique identifier of the feed profile associated with this schedule.
313 // * `profile_name` - The name of the feed profile associated with this schedule.
314 // * `is_weekly` - A boolean flag indicating if this schedule entry should repeat weekly.
315 // * `is_daily` - A boolean flag indicating if this schedule entry should repeat daily.
316 //
317 // # Returns
318 // An empty `Result` (`Ok(())`) if operation was successful.
319 //
320 // # Errors
321 // This function will return an `InsertFeedEventFailure` if the database operation fails.
322 #[cfg(test)]
323 fn insert_feed_schedule_entry(
324 &mut self,
325 timestamp: NaiveDateTime,
326 profile_id: i32,
327 profile_name: String,
328 is_weekly: bool,
329 is_daily: bool,
330 ) -> mysql::Result<(), SqlInterfaceError> {
331 let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEED_SCHEDULE_ENTRY.to_owned();
332 match self.conn.exec_drop::<_, _>(
333 sql_query_string,
334 params! {
335 "timestamp" => timestamp,
336 "profile_id" => profile_id,
337 "profile_name" => profile_name,
338 "is_weekly" => is_weekly,
339 "is_daily" => is_daily,
340 },
341 ) {
342 Ok(_) => Ok(()),
343 Err(e) => {
344 error!( // logging for testing purposes only
345 target: module_path!(),
346 "could not insert feed schedule entry ({e:?})"
347 );
348 Err(SqlInterfaceError::InsertFeedEventFailure {
349 location: module_path!().to_string(),
350 source: e,
351 })
352 }
353 }
354 }
355}