aquarium_control/database/
sql_interface_feed.rs

1/* Copyright 2024 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//! Manages all database interactions for the automated feeding system.
11//!
12//! This module provides a dedicated interface, `SqlInterfaceFeed`, for handling
13//! all SQL operations related to the feeding schedule (`feedschedule`), feed patterns
14//! (`feedpatterns`), and feed event logging (`feedlog`).
15//!
16//! ## Key Components
17//!
18//! - **`SqlInterfaceFeed` Struct**: The primary struct that holds a database
19//!   connection and provides methods for all feed-related database operations.
20//!
21//! - **`new()` Constructor**: A critical entry point that not only creates an
22//!   `SqlInterfaceFeed` instance but also performs essential "fail-fast"
23//!   validation. At initialization, it checks the database for:
24//!   - The presence of `NULL` values in the `feedpatterns` and `feedschedule` tables.
25//!   - Whether the number of rows in the `feedpatterns`, `feedschedule`, and `feedlog`
26//!     tables exceeds the limits defined in the application configuration.
27//!   This ensures the system doesn't start in an invalid or inconsistent state.
28//!
29//! - **Data-Fetching Methods**: Functions like `get_feedpattern_header_from_database`
30//!   and `get_feed_phase_from_database` provide a structured way to retrieve
31//!   complex feed pattern data from the database.
32//!
33//! ## Design and Purpose
34//!
35//! The main goal of this module is to provide a robust and encapsulated interface
36//! for the feeding subsystem's data.
37//!
38//! - **Encapsulation**: All SQL queries and logic specific to feeding are contained
39//!   within this module, separating concerns and making the overall database
40//!   architecture cleaner.
41//!
42//! - **Data Integrity**: The constructor's validation logic enforces data integrity
43//!   rules at startup, preventing runtime errors caused by misconfigured or
44//!   overgrown database tables
45
46use chrono::NaiveDateTime;
47use mysql::prelude::*;
48use mysql::*;
49
50use crate::database::{
51    sql_interface::SqlInterface, sql_interface_error::SqlInterfaceError, sql_query_strings,
52};
53use crate::food::feed_schedule_entry::FeedScheduleEntry;
54
55#[derive(Clone)]
56/// Holds the header information of a feed pattern.
57pub struct SqlFeedpatternHeader {
58    pub profile_id: i32,
59    pub profile_name: String,
60}
61
62#[derive(Clone)]
63/// Holds the data of one feed pattern phase as retrieved from the database.
64/// The numeric data is post-processed to boolean in a further step.
65pub struct SqlFeedPhase {
66    /// duration of the pause before switching on the feeder motor in milliseconds
67    pub pause_duration: i16,
68
69    /// flag indicating if the protein skimmer shall remain switched on during the pause
70    pub pause_skimmer: i16,
71
72    /// flag indicating if the main pump #1 shall remain switched on during the pause
73    pub pause_main_pump_1: i16,
74
75    /// flag indicating if the main pump #2 shall remain switched on during the pause
76    pub pause_main_pump_2: i16,
77
78    /// flag indicating if the auxiliary pump #1 shall remain switched on during the pause
79    pub pause_aux_pump_1: i16,
80
81    /// flag indicating if the auxiliary pump #2 shall remain switched on during the pause
82    pub pause_aux_pump_2: i16,
83
84    /// duration of the feed after switching on the feeder motor in milliseconds
85    pub feed_duration: i16,
86
87    /// flag indicating if the protein skimmer shall remain switched on during the feed phase
88    pub feed_skimmer: i16,
89
90    /// flag indicating if the main pump #1 shall remain switched on during the feed phase
91    pub feed_main_pump_1: i16,
92
93    /// flag indicating if the main pump #2 shall remain switched on during the feed phase
94    pub feed_main_pump_2: i16,
95
96    /// flag indicating if the auxiliary pump #1 shall remain switched on during the feed phase
97    pub feed_aux_pump_1: i16,
98
99    /// flag indicating if the auxiliary pump #2 shall remain switched on during the feed phase
100    pub feed_aux_pump_2: i16,
101}
102
103/// Holds the data of a feed schedule entry as retrieved from the database.
104/// It is post-processed in a further step.
105pub struct SqlFeedScheduleEntry {
106    pub timestamp: NaiveDateTime,
107    pub profile_id: i32,
108    pub profile_name: String,
109    pub repeat_daily: i16,
110}
111
112/// Contains the configuration and the implementation of the SQL interface for Feed.
113#[derive(Debug)]
114pub struct SqlInterfaceFeed {
115    /// connection to SQL database
116    pub conn: PooledConn,
117}
118
119impl SqlInterfaceFeed {
120    /// Creates a new `SqlInterfaceFeed` instance.
121    ///
122    /// This constructor initializes the feed management SQL interface with an
123    /// established database connection. It performs several pre-flight checks to
124    /// ensure data integrity and adherence to configured limits, such as verifying
125    /// that tables contain no NULL values and that row counts are within their
126    /// specified maximums.
127    ///
128    /// # Arguments
129    /// * `conn` - An active, pooled database connection.
130    /// * `max_rows_feed_pattern` - The maximum allowed rows in the `feedpatterns` table.
131    /// * `max_rows_feed_schedule` - The maximum allowed rows in the `feedschedule` table.
132    /// * `max_rows_feed_log` - The maximum allowed rows in the `feedlog` table.
133    ///
134    /// # Returns
135    /// A `Result` containing a new `SqlInterfaceFeed` instance on success.
136    ///
137    /// # Errors
138    /// This function will return an error if:
139    /// - Any of the initial database queries to get table counts or check for NULLs fail (`DatabaseCheckFeedSetValsFailure`).
140    /// - Any of the retrieved counts are negative, indicating a database issue (`DatabaseFeedTableNegativeValue`).
141    /// - The `feedpatterns` or `feedschedule` tables contain entries with `NULL` values (`DatabaseFeedSetValTableContainsNull`).
142    /// - The number of rows in any of the checked tables exceeds its configured maximum (`DatabaseFeedTableContainsTooManyRows`). .
143    pub fn new(
144        mut conn: PooledConn,
145        max_rows_feed_pattern: u64,
146        max_rows_feed_schedule: u64,
147        max_rows_feed_log: u64,
148    ) -> Result<Self, SqlInterfaceError> {
149        // check if feed pattern table is consistent
150        let count_null_values_feedpatterns = SqlInterface::get_single_integer_from_database(
151            &mut conn,
152            sql_query_strings::SQL_QUERY_CHECK_FEEDPATTERNS_NULL,
153        )
154        .map_err(|e| SqlInterfaceError::DatabaseCheckFeedSetValsFailure {
155            location: module_path!().to_string(),
156            table_name: sql_query_strings::SQL_TABLE_FEEDPATTERNS.to_string(),
157            source: Box::new(e),
158        })?;
159        let count_rows_feedpatterns = SqlInterface::get_single_integer_from_database(
160            &mut conn,
161            sql_query_strings::SQL_QUERY_CHECK_FEEDPATTERN_COUNT,
162        )
163        .map_err(|e| SqlInterfaceError::DatabaseCheckFeedSetValsFailure {
164            location: module_path!().to_string(),
165            table_name: sql_query_strings::SQL_TABLE_FEEDPATTERNS.to_string(),
166            source: Box::new(e),
167        })?;
168
169        // check if the feed schedule table is consistent
170        let count_null_values_feedschedule = SqlInterface::get_single_integer_from_database(
171            &mut conn,
172            sql_query_strings::SQL_QUERY_CHECK_FEEDSCHEDULE_NULL,
173        )
174        .map_err(|e| SqlInterfaceError::DatabaseCheckFeedSetValsFailure {
175            location: module_path!().to_string(),
176            table_name: sql_query_strings::SQL_TABLE_FEEDSCHEDULE.to_string(),
177            source: Box::new(e),
178        })?;
179        let count_rows_feedschedule = SqlInterface::get_single_integer_from_database(
180            &mut conn,
181            sql_query_strings::SQL_QUERY_CHECK_FEEDSCHEDULE_COUNT,
182        )
183        .map_err(|e| SqlInterfaceError::DatabaseCheckFeedSetValsFailure {
184            location: module_path!().to_string(),
185            table_name: sql_query_strings::SQL_TABLE_FEEDPATTERNS.to_string(),
186            source: Box::new(e),
187        })?;
188
189        let count_rows_feedlog = SqlInterface::get_single_integer_from_database(
190            &mut conn,
191            sql_query_strings::SQL_QUERY_CHECK_FEEDLOG_COUNT,
192        )
193        .map_err(|e| SqlInterfaceError::DatabaseCheckFeedSetValsFailure {
194            location: module_path!().to_string(),
195            table_name: sql_query_strings::SQL_TABLE_FEEDLOG.to_string(),
196            source: Box::new(e),
197        })?;
198
199        // check the query results
200        if count_null_values_feedpatterns < 0 || count_rows_feedpatterns < 0 {
201            return Err(SqlInterfaceError::DatabaseFeedTableNegativeValue(
202                module_path!().to_string(),
203                sql_query_strings::SQL_TABLE_FEEDPATTERNS.to_string(),
204                count_null_values_feedpatterns,
205                count_rows_feedpatterns,
206            ));
207        }
208        if count_null_values_feedschedule < 0 || count_rows_feedschedule < 0 {
209            return Err(SqlInterfaceError::DatabaseFeedTableNegativeValue(
210                module_path!().to_string(),
211                sql_query_strings::SQL_TABLE_FEEDSCHEDULE.to_string(),
212                count_null_values_feedschedule,
213                count_rows_feedschedule,
214            ));
215        }
216        if count_rows_feedlog < 0 {
217            return Err(SqlInterfaceError::DatabaseFeedTableNegativeValue(
218                module_path!().to_string(),
219                sql_query_strings::SQL_TABLE_FEEDLOG.to_string(),
220                -1,
221                count_rows_feedlog,
222            ));
223        }
224
225        // check if there are any NULL values in the feed set value tables
226        if count_null_values_feedpatterns > 0 {
227            return Err(SqlInterfaceError::DatabaseFeedSetValTableContainsNull(
228                module_path!().to_string(),
229                sql_query_strings::SQL_QUERY_CHECK_FEEDPATTERNS_NULL.to_string(),
230                count_null_values_feedpatterns,
231            ));
232        }
233        if count_null_values_feedschedule > 0 {
234            return Err(SqlInterfaceError::DatabaseFeedSetValTableContainsNull(
235                module_path!().to_string(),
236                sql_query_strings::SQL_QUERY_CHECK_FEEDSCHEDULE_NULL.to_string(),
237                count_null_values_feedschedule,
238            ));
239        }
240
241        if max_rows_feed_pattern > 0 {
242            // execute the check only when the limit is greater than zero
243            if count_rows_feedpatterns as u64 > max_rows_feed_pattern {
244                return Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
245                    module_path!().to_string(),
246                    sql_query_strings::SQL_TABLE_FEEDPATTERNS.to_string(),
247                    count_rows_feedpatterns as u64,
248                    max_rows_feed_pattern,
249                ));
250            }
251        }
252
253        if max_rows_feed_schedule > 0 {
254            // execute the check only when the limit is greater than zero
255            if count_rows_feedschedule as u64 > max_rows_feed_schedule {
256                return Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
257                    module_path!().to_string(),
258                    sql_query_strings::SQL_TABLE_FEEDSCHEDULE.to_string(),
259                    count_rows_feedschedule as u64,
260                    max_rows_feed_schedule,
261                ));
262            }
263        }
264
265        if max_rows_feed_log > 0 {
266            // execute the check only when the limit is greater than zero
267            if count_rows_feedlog as u64 > max_rows_feed_log {
268                return Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
269                    module_path!().to_string(),
270                    sql_query_strings::SQL_TABLE_FEEDLOG.to_string(),
271                    count_rows_feedlog as u64,
272                    max_rows_feed_log,
273                ));
274            }
275        }
276
277        Ok(SqlInterfaceFeed { conn })
278    }
279
280    /// Retrieves and post-processes feed schedule entries from the database based on a given SQL query.
281    ///
282    /// This private helper function executes the provided `sql_query` to fetch raw schedule data.
283    /// It then attempts to convert these raw `SqlScheduleEntry` records into fully typed
284    /// `FeedScheduleEntry` structs.
285    ///
286    /// # Arguments
287    /// * `sql_query` - The SQL query string used to retrieve feed schedule entries.
288    ///
289    /// # Returns
290    /// A `Result` containing an `Option<Vec<FeedScheduleEntry>>`:
291    /// - `Ok(Some(Vec<FeedScheduleEntry>))`: If one or more schedule entries are successfully retrieved and converted.
292    /// - `Ok(None)`: If the query returns no rows.
293    ///
294    /// # Errors
295    /// This function will return an error if:
296    /// - The underlying database query fails (`FeedScheduleEntryRequestFailure`). This can be due to a connection issue or an invalid query.
297    /// - The query returns results, but one of the entries cannot be processed (e.g., due to a malformed timestamp), resulting in `FeedScheduleEntryTimeStampProcessingFailure`.
298    pub(crate) fn get_feedschedule_entries_from_database(
299        &mut self,
300        sql_query: &str,
301    ) -> Result<Option<Vec<FeedScheduleEntry>>, SqlInterfaceError> {
302        let sql_feedschedule_entry_array = match self.conn.query_map(
303            sql_query,
304            |(timestamp, profile_id, profile_name, repeat_daily)| SqlFeedScheduleEntry {
305                timestamp,
306                profile_id,
307                profile_name,
308                repeat_daily,
309            },
310        ) {
311            Ok(c) => c,
312            Err(e) => {
313                return Err(SqlInterfaceError::FeedScheduleEntryRequestFailure {
314                    location: module_path!().to_string(),
315                    query: sql_query.to_string(),
316                    source: e,
317                });
318            }
319        };
320
321        // return none if the query result is empty
322        if sql_feedschedule_entry_array.is_empty() {
323            return Ok(None);
324        }
325
326        let feedschedule_entries_result: Result<Vec<FeedScheduleEntry>, _> =
327            sql_feedschedule_entry_array
328                .iter()
329                .map(FeedScheduleEntry::new)
330                .collect();
331
332        // Handle conversion errors that may occur during the mapping.
333        let feedschedule_entries = feedschedule_entries_result.map_err(|e| {
334            SqlInterfaceError::FeedScheduleEntryTimeStampProcessingFailure {
335                location: module_path!().to_string(),
336                number_entries: sql_feedschedule_entry_array.len(),
337                source: Box::new(e),
338            }
339        })?;
340
341        if feedschedule_entries.is_empty() {
342            Ok(None)
343        } else {
344            Ok(Some(feedschedule_entries))
345        }
346    }
347
348    #[cfg(test)]
349    // Retrieves the single feed schedule entry that is furthest in the future.
350    //
351    // This private helper function is intended for test scenarios. It queries the database
352    // for future-scheduled feed entries and, assuming they are ordered by timestamp,
353    // returns the earliest (furthest in the future) one.
354    //
355    // # Returns
356    // A `Result` which is:
357    // - `Ok(Some(FeedScheduleEntry))` if a future feed schedule entry is found.
358    // - `Ok(None)` if no future feed schedule entries are found in the database.
359    // - `Err(SqlInterfaceError)` if an error occurs during the database query
360    //   or post-processing of the data.
361    fn get_future_feedschedule_entry_from_database(
362        &mut self,
363    ) -> Result<Option<FeedScheduleEntry>, SqlInterfaceError> {
364        let entries_opt = self.get_feedschedule_entries_from_database(
365            sql_query_strings::SQL_QUERY_READ_FUTURE_FEED_SCHEDULE_ENTRY,
366        )?;
367        Ok(entries_opt.and_then(|mut vec| vec.pop()))
368    }
369
370    #[cfg(test)]
371    // Retrieves all feed schedule entries that are scheduled in the future.
372    //
373    // This private helper function is intended for test scenarios. It queries the database
374    // for all feed entries whose timestamps are in the future, then processes them
375    // into a vector of `FeedScheduleEntry` structs.
376    //
377    // # Returns
378    // A `Result` which is:
379    // - `Ok(Some(Vec<FeedScheduleEntry>))` if one or more future feed schedule entries are found.
380    // - `Ok(None)` if no future feed schedule entries are found in the database.
381    // - `Err(SqlInterfaceError)` if an error occurs during the database query
382    //   or post-processing of the data.
383    fn get_future_feedschedule_entries_from_database(
384        &mut self,
385    ) -> Result<Option<Vec<FeedScheduleEntry>>, SqlInterfaceError> {
386        self.get_feedschedule_entries_from_database(
387            sql_query_strings::SQL_QUERY_READ_FUTURE_FEED_SCHEDULE_ENTRY,
388        )
389    }
390
391    /// Retrieves the header (basic information) of a specific feed pattern from the database.
392    ///
393    /// This function queries the database using the provided `profile_id` to fetch
394    /// the feed pattern's unique ID and its associated name. It strictly expects
395    /// exactly one matching header entry.
396    ///
397    /// # Arguments
398    /// * `profile_id` - The unique identifier of the feed profile whose header is to be retrieved.
399    ///
400    /// # Returns
401    /// A `Result` which is:
402    /// - `Ok(SqlFeedpatternHeader)` if exactly one matching header entry is found.
403    /// - `Err(SqlInterfaceError::SingleFeedpatternRequestFailure)` if the database query fails.
404    /// - `Err(SqlInterfaceError::SingleFeedpatternRequestNoSingleResponse)` if the query
405    ///   returns no entry or more than one entry for the given `profile_id`.
406    pub fn get_feedpattern_header_from_database(
407        &mut self,
408        profile_id: i32,
409    ) -> Result<SqlFeedpatternHeader, SqlInterfaceError> {
410        // prepare the query for header
411        let params = params! { "profile_id" => profile_id};
412
413        let single_feedpattern_entry_array = self
414            .conn
415            .exec_map(
416                sql_query_strings::SQL_QUERY_READ_FEEDPATTERN_HEADER,
417                params,
418                |(profile_id, profile_name)| SqlFeedpatternHeader {
419                    profile_id,
420                    profile_name,
421                },
422            )
423            .map_err(|e| SqlInterfaceError::SingleFeedpatternRequestFailure {
424                location: module_path!().to_string(),
425                query: sql_query_strings::SQL_QUERY_READ_FEEDPATTERN_HEADER.to_string(),
426                profile_id,
427                source: e,
428            })?;
429
430        // check the error case if there is no response
431        if single_feedpattern_entry_array.is_empty() {
432            return Err(SqlInterfaceError::SingleFeedpatternRequestEmptyResponse(
433                module_path!().to_string(),
434                sql_query_strings::SQL_QUERY_READ_FEEDPATTERN_HEADER.to_string(),
435                profile_id,
436            ));
437        }
438
439        // check the error case if there is more than one response
440        if single_feedpattern_entry_array.len() > 1 {
441            return Err(SqlInterfaceError::SingleFeedpatternRequestNoSingleResponse(
442                module_path!().to_string(),
443                sql_query_strings::SQL_QUERY_READ_FEEDPATTERN_HEADER.to_string(),
444                profile_id,
445            ));
446        }
447
448        let feedpattern_header = &single_feedpattern_entry_array[0];
449
450        Ok(feedpattern_header.clone())
451    }
452
453    /// Retrieves the specific data for a single phase of a feed pattern from the database.
454    ///
455    /// This function constructs a query using both the `profile_id` and the `phase_nr`
456    /// to fetch the detailed configuration for that particular feed phase. It strictly
457    /// expects to find exactly one matching phase entry.
458    ///
459    /// # Arguments
460    /// * `profile_id` - The unique identifier of the feed profile.
461    /// * `phase_nr` - The specific phase number (e.g., 1, 2, ..., 10) within the feed profile.
462    ///
463    /// # Returns
464    /// A `Result` which is:
465    /// - `Ok(SqlFeedPhase)` if exactly one matching phase entry is found for the given profile and phase number.
466    /// - `Err(SqlInterfaceError::SingleFeedpatternRequestFailure)` if the database query fails.
467    /// - `Err(SqlInterfaceError::SingleFeedpatternRequestNoSingleResponse)` if the query
468    ///   returns no entry or more than one entry for the specified profile and phase.
469    pub fn get_feed_phase_from_database(
470        &mut self,
471        profile_id: i32,
472        phase_nr: i32,
473    ) -> Result<SqlFeedPhase, SqlInterfaceError> {
474        let params = params!("profile_id" => profile_id);
475        let sql_query = str::replace(
476            sql_query_strings::SQL_QUERY_READ_FEED_PHASE,
477            "#",
478            format!("{phase_nr:02}").as_str(),
479        );
480
481        let single_feedpattern_entry_array = match self.conn.exec_map(
482            sql_query.clone(),
483            params,
484            |(
485                pause_duration,
486                pause_skimmer,
487                pause_main_pump_1,
488                pause_main_pump_2,
489                pause_aux_pump_1,
490                pause_aux_pump_2,
491                feed_duration,
492                feed_skimmer,
493                feed_main_pump_1,
494                feed_main_pump_2,
495                feed_aux_pump_1,
496                feed_aux_pump_2,
497            )| SqlFeedPhase {
498                pause_duration,
499                pause_skimmer,
500                pause_main_pump_1,
501                pause_main_pump_2,
502                pause_aux_pump_1,
503                pause_aux_pump_2,
504                feed_duration,
505                feed_skimmer,
506                feed_main_pump_1,
507                feed_main_pump_2,
508                feed_aux_pump_1,
509                feed_aux_pump_2,
510            },
511        ) {
512            Ok(c) => c,
513            Err(e) => {
514                return Err(SqlInterfaceError::SingleFeedpatternRequestFailure {
515                    location: module_path!().to_string(),
516                    query: sql_query,
517                    profile_id,
518                    source: e,
519                });
520            }
521        };
522
523        // check the error case if there is no response
524        if single_feedpattern_entry_array.is_empty() {
525            return Err(SqlInterfaceError::SingleFeedpatternRequestEmptyResponse(
526                module_path!().to_string(),
527                sql_query,
528                profile_id,
529            ));
530        }
531
532        // check the error case if there is more than one response
533        if single_feedpattern_entry_array.len() > 1 {
534            return Err(SqlInterfaceError::SingleFeedpatternRequestNoSingleResponse(
535                module_path!().to_string(),
536                sql_query,
537                profile_id,
538            ));
539        }
540
541        let feed_phase = &single_feedpattern_entry_array[0];
542
543        // check if pause and feed duration values are positive
544        if feed_phase.pause_duration < 0 || feed_phase.feed_duration < 0 {
545            return Err(SqlInterfaceError::DatabaseFeedPhaseDurationNegative(
546                module_path!().to_string(),
547                feed_phase.pause_duration,
548                feed_phase.feed_duration,
549            ));
550        }
551        Ok(feed_phase.clone())
552    }
553}
554
555#[cfg(test)]
556pub mod tests {
557    use chrono::{Duration, Local, NaiveDate, NaiveDateTime};
558    use mysql::{params, prelude::Queryable};
559
560    use crate::database::database_interface_feed_trait::DatabaseInterfaceFeedTrait;
561    use crate::database::sql_interface_feed::{FeedScheduleEntry, SqlInterfaceFeed};
562    use crate::database::sql_query_strings;
563    use crate::database::sql_query_strings::{
564        SQL_TABLE_FEEDLOG, SQL_TABLE_FEEDPATTERNS, SQL_TABLE_FEEDSCHEDULE,
565    };
566    use crate::database::{sql_interface::SqlInterface, sql_interface_error::SqlInterfaceError};
567    use crate::utilities::config::{read_config_file_with_test_database, ConfigData};
568
569    pub struct SqlFeedEvent {
570        timestamp: String,
571        feeder_run_time: f32,
572        feed_profile_name: String,
573        feed_profile_id: i32,
574    }
575
576    pub struct FeedEvent {
577        timestamp: NaiveDateTime,
578        feeder_run_time: f32,
579        feed_profile_name: String,
580        feed_profile_id: i32,
581    }
582
583    #[cfg(test)]
584    // This helper function prepares the database for feed-related test cases.
585    // It truncates the 'feedpatterns' table to ensure a clean state and then
586    // inserts two predefined test feed profiles.
587    //
588    // Arguments:
589    // * `sql_interface`: A mutable reference to the general SQL interface, used for table truncation.
590    // * `sql_interface_feed`: A mutable reference to the feed-specific SQL interface, used for inserting patterns.
591    pub fn insert_feed_patterns(
592        sql_interface: &mut SqlInterface,
593        sql_interface_feed: &mut SqlInterfaceFeed,
594    ) {
595        // remove all feed patterns
596        match SqlInterface::truncate_table(sql_interface, SQL_TABLE_FEEDPATTERNS.to_string()) {
597            Ok(_) => {}
598            Err(e) => panic!("Could not prepare test case: {e:?}"),
599        }
600        // insert test feed profile #1
601        let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEEDPATTERN_TEST.to_string();
602        match sql_interface_feed.conn.exec_drop::<_, _>(
603            sql_query_string,
604            params! {
605            "profile_id" => 1, "profile_name" => "test"
606            },
607        ) {
608            Ok(_) => {}
609            Err(e) => {
610                panic!("Error when inserting test feed pattern #1 into database: {e:?}");
611            }
612        };
613        // insert test feed profile #2
614        let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEEDPATTERN_TEST2.to_string();
615        match sql_interface_feed.conn.exec_drop::<_, _>(
616            sql_query_string,
617            params! {
618            "profile_id" => 2,
619            },
620        ) {
621            Ok(_) => {}
622            Err(e) => {
623                panic!("Error when inserting test feed pattern #2 into database: {e:?}");
624            }
625        };
626    }
627
628    #[test]
629    // This test case verifies the validation logic within the `SqlInterfaceFeed::new()` constructor.
630    // It covers the following scenarios:
631    // 1. Happy Path: Initialization succeeds with valid data and row counts.
632    // 2. Failure on Row Limits: Fails when any of the feed-related tables exceed their max row count.
633    // 3. Deactivated Checks: Succeeds even with excess rows if the limits are set to 0.
634    // Test case uses test database #59.
635    fn test_sql_interface_feed_new() {
636        // --- Common Setup ---
637        let config: ConfigData = read_config_file_with_test_database(
638            "/config/aquarium_control_test_generic.toml".to_string(),
639            59,
640        );
641        println!("Testing with database {}", config.sql_interface.db_name);
642        let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
643            .expect("Initialization of SQL interface for test failed.");
644
645        // --- Test Case 1: Happy Path ---
646        println!("* Testing new() with valid data (Happy Path)...");
647        SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDPATTERNS.to_string())
648            .unwrap();
649        SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string())
650            .unwrap();
651        SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDLOG.to_string()).unwrap();
652
653        // Insert one valid row into each table
654        let mut conn = sql_interface.get_connection().unwrap();
655        conn.exec_drop(
656            sql_query_strings::SQL_QUERY_WRITE_FEEDPATTERN_TEST,
657            params! { "profile_id" => 1, "profile_name" => "test_profile1" },
658        )
659        .unwrap();
660        conn.exec_drop(
661            sql_query_strings::SQL_QUERY_WRITE_FEED_SCHEDULE_ENTRY,
662            params! { "timestamp" => "2024-01-01 12:00:00", "profile_id" => 1, "profile_name" => "test_profile1", "is_weekly" => false, "is_daily" => true },
663        ).unwrap();
664        conn.exec_drop(
665            sql_query_strings::SQL_QUERY_WRITE_FEED_EVENT,
666            params! { "timestamp" => Local::now().naive_local(), "feeder_run_time" => 1.0, "profile_name" => "test_profile1", "profile_id" => 1 },
667        ).unwrap();
668
669        let result = SqlInterfaceFeed::new(conn, 10, 10, 10);
670        assert!(
671            result.is_ok(),
672            "Expected new() to succeed on happy path, but it failed: {:?}",
673            result.err()
674        );
675        println!("* Succeeded: Happy path initialization is successful.");
676
677        // --- Test Case 2: Failure on feedpatterns row limit ---
678        println!("* Testing new() with too many rows in feedpatterns...");
679        SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDPATTERNS.to_string())
680            .unwrap();
681        let mut conn = sql_interface.get_connection().unwrap();
682        conn.exec_drop(
683            sql_query_strings::SQL_QUERY_WRITE_FEEDPATTERN_TEST,
684            params! { "profile_id" => 1, "profile_name" => "row1" },
685        )
686        .unwrap();
687        conn.exec_drop(
688            sql_query_strings::SQL_QUERY_WRITE_FEEDPATTERN_TEST,
689            params! { "profile_id" => 2, "profile_name" => "row2" },
690        )
691        .unwrap();
692
693        // Set the limit to 1
694        let result = SqlInterfaceFeed::new(conn, 1, 10, 10);
695        assert!(
696            matches!(
697                result,
698                Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
699                    _,
700                    _,
701                    _,
702                    _
703                ))
704            ),
705            "Expected row limit error for feedpatterns, but got {:?}",
706            result
707        );
708        println!("* Succeeded: Initialization fails if feedpatterns exceeds row limit.");
709
710        // --- Test Case 4: Deactivated checks (limits are 0) ---
711        println!("* Testing new() with deactivated row limit checks (limits = 0)...");
712        // We reuse the state from the previous test (2 rows in feedpatterns)
713        let conn = sql_interface.get_connection().unwrap();
714        // Set all the limits to 0
715        let result = SqlInterfaceFeed::new(conn, 0, 0, 0);
716        assert!(
717            result.is_ok(),
718            "Expected new() to succeed with deactivated checks, but it failed: {:?}",
719            result.err()
720        );
721        println!("* Succeeded: Initialization passes when row limit checks are deactivated.");
722    }
723
724    #[test]
725    // Test case includes all checks for this table combined in one function to limit the number of additional databases.
726    // Test case uses test database #33.
727    pub fn test_sql_interface_feedpattern() {
728        let config: ConfigData = read_config_file_with_test_database(
729            "/config/aquarium_control_test_generic.toml".to_string(),
730            33,
731        );
732        println!("Testing with database {}", config.sql_interface.db_name);
733        let max_rows_feed_pattern = config.sql_interface.max_rows_feed_pattern;
734        let max_rows_feed_schedule = config.sql_interface.max_rows_feed_schedule;
735        let max_rows_feed_log = config.sql_interface.max_rows_feed_log;
736        let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
737            .expect("Initialization of SQL interface for test failed.");
738        let mut sql_interface_feed = SqlInterfaceFeed::new(
739            sql_interface.get_connection().unwrap(),
740            max_rows_feed_pattern,
741            max_rows_feed_schedule,
742            max_rows_feed_log,
743        )
744        .unwrap();
745
746        //*** check retrieving of the feed pattern from empty table *********************************
747        match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDPATTERNS.to_string()) {
748            Ok(_) => {}
749            Err(e) => panic!("Could not prepare test case: {e:?}"),
750        }
751        match sql_interface_feed.get_single_feedpattern_from_database(1) {
752            Ok(_) => {
753                panic!("Call to get_single_feedpattern_from_database should have failed.");
754            }
755            Err(e) => {
756                assert!(matches!(
757                    e,
758                    SqlInterfaceError::SingleFeedpatternRequestEmptyResponse(_, _, _)
759                ));
760            }
761        }
762        println!("* checking retrieving of Feed pattern (FAIL) succeeded.");
763        // **************************************************************************************
764
765        // *** check retrieving of existing feed pattern ****************************************
766        // prepare database: insert feed profile
767        let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEEDPATTERN_TEST.to_string();
768        match sql_interface_feed.conn.exec_drop::<_, _>(
769            sql_query_string.clone(),
770            params! {
771            "profile_id" => 1, "profile_name" => "test",
772            },
773        ) {
774            Ok(_) => {}
775            Err(e) => {
776                panic!("Error when inserting feed pattern into database: {e:?}");
777            }
778        };
779        match sql_interface_feed.get_single_feedpattern_from_database(1) {
780            Ok(c) => {
781                assert_eq!(c.profile_id, 1);
782                assert_eq!(c.profile_name, "test".to_string());
783                assert_eq!(c.feedphases[0].pause_duration, 0);
784                assert_eq!(c.feedphases[0].pause_skimmer, true);
785                assert_eq!(c.feedphases[0].pause_main_pump_1, false);
786                assert_eq!(c.feedphases[0].pause_main_pump_2, false);
787                assert_eq!(c.feedphases[0].pause_aux_pump_1, false);
788                assert_eq!(c.feedphases[0].pause_aux_pump_2, false);
789                assert_eq!(c.feedphases[0].feed_duration, 1);
790                assert_eq!(c.feedphases[0].feed_skimmer, false);
791                assert_eq!(c.feedphases[0].feed_main_pump_1, true);
792                assert_eq!(c.feedphases[0].feed_main_pump_2, false);
793                assert_eq!(c.feedphases[0].feed_aux_pump_1, false);
794                assert_eq!(c.feedphases[0].feed_aux_pump_2, false);
795
796                assert_eq!(c.feedphases[1].pause_duration, 2);
797                assert_eq!(c.feedphases[1].pause_skimmer, false);
798                assert_eq!(c.feedphases[1].pause_main_pump_1, false);
799                assert_eq!(c.feedphases[1].pause_main_pump_2, true);
800                assert_eq!(c.feedphases[1].pause_aux_pump_1, false);
801                assert_eq!(c.feedphases[1].pause_aux_pump_2, false);
802                assert_eq!(c.feedphases[1].feed_duration, 3);
803                assert_eq!(c.feedphases[1].feed_skimmer, false);
804                assert_eq!(c.feedphases[1].feed_main_pump_1, false);
805                assert_eq!(c.feedphases[1].feed_main_pump_2, false);
806                assert_eq!(c.feedphases[1].feed_aux_pump_1, true);
807                assert_eq!(c.feedphases[1].feed_aux_pump_2, false);
808
809                assert_eq!(c.feedphases[2].pause_duration, 4);
810                assert_eq!(c.feedphases[2].pause_skimmer, false);
811                assert_eq!(c.feedphases[2].pause_main_pump_1, false);
812                assert_eq!(c.feedphases[2].pause_main_pump_2, false);
813                assert_eq!(c.feedphases[2].pause_aux_pump_1, false);
814                assert_eq!(c.feedphases[2].pause_aux_pump_2, true);
815                assert_eq!(c.feedphases[2].feed_duration, 5);
816                assert_eq!(c.feedphases[2].feed_skimmer, true);
817                assert_eq!(c.feedphases[2].feed_main_pump_1, true);
818                assert_eq!(c.feedphases[2].feed_main_pump_2, false);
819                assert_eq!(c.feedphases[2].feed_aux_pump_1, false);
820                assert_eq!(c.feedphases[2].feed_aux_pump_2, false);
821
822                assert_eq!(c.feedphases[3].pause_duration, 6);
823                assert_eq!(c.feedphases[3].pause_skimmer, true);
824                assert_eq!(c.feedphases[3].pause_main_pump_1, false);
825                assert_eq!(c.feedphases[3].pause_main_pump_2, true);
826                assert_eq!(c.feedphases[3].pause_aux_pump_1, false);
827                assert_eq!(c.feedphases[3].pause_aux_pump_2, false);
828                assert_eq!(c.feedphases[3].feed_duration, 7);
829                assert_eq!(c.feedphases[3].feed_skimmer, true);
830                assert_eq!(c.feedphases[3].feed_main_pump_1, false);
831                assert_eq!(c.feedphases[3].feed_main_pump_2, false);
832                assert_eq!(c.feedphases[3].feed_aux_pump_1, true);
833                assert_eq!(c.feedphases[3].feed_aux_pump_2, false);
834
835                assert_eq!(c.feedphases[4].pause_duration, 8);
836                assert_eq!(c.feedphases[4].pause_skimmer, true);
837                assert_eq!(c.feedphases[4].pause_main_pump_1, false);
838                assert_eq!(c.feedphases[4].pause_main_pump_2, false);
839                assert_eq!(c.feedphases[4].pause_aux_pump_1, false);
840                assert_eq!(c.feedphases[4].pause_aux_pump_2, true);
841                assert_eq!(c.feedphases[4].feed_duration, 9);
842                assert_eq!(c.feedphases[4].feed_skimmer, true);
843                assert_eq!(c.feedphases[4].feed_main_pump_1, true);
844                assert_eq!(c.feedphases[4].feed_main_pump_2, true);
845                assert_eq!(c.feedphases[4].feed_aux_pump_1, false);
846                assert_eq!(c.feedphases[4].feed_aux_pump_2, false);
847
848                assert_eq!(c.feedphases[5].pause_duration, 10);
849                assert_eq!(c.feedphases[5].pause_skimmer, true);
850                assert_eq!(c.feedphases[5].pause_main_pump_1, true);
851                assert_eq!(c.feedphases[5].pause_main_pump_2, true);
852                assert_eq!(c.feedphases[5].pause_aux_pump_1, false);
853                assert_eq!(c.feedphases[5].pause_aux_pump_2, false);
854                assert_eq!(c.feedphases[5].feed_duration, 11);
855                assert_eq!(c.feedphases[5].feed_skimmer, true);
856                assert_eq!(c.feedphases[5].feed_main_pump_1, true);
857                assert_eq!(c.feedphases[5].feed_main_pump_2, false);
858                assert_eq!(c.feedphases[5].feed_aux_pump_1, false);
859                assert_eq!(c.feedphases[5].feed_aux_pump_2, true);
860
861                assert_eq!(c.feedphases[6].pause_duration, 12);
862                assert_eq!(c.feedphases[6].pause_skimmer, true);
863                assert_eq!(c.feedphases[6].pause_main_pump_1, true);
864                assert_eq!(c.feedphases[6].pause_main_pump_2, true);
865                assert_eq!(c.feedphases[6].pause_aux_pump_1, true);
866                assert_eq!(c.feedphases[6].pause_aux_pump_2, false);
867                assert_eq!(c.feedphases[6].feed_duration, 13);
868                assert_eq!(c.feedphases[6].feed_skimmer, true);
869                assert_eq!(c.feedphases[6].feed_main_pump_1, true);
870                assert_eq!(c.feedphases[6].feed_main_pump_2, true);
871                assert_eq!(c.feedphases[6].feed_aux_pump_1, false);
872                assert_eq!(c.feedphases[6].feed_aux_pump_2, true);
873
874                assert_eq!(c.feedphases[7].pause_duration, 14);
875                assert_eq!(c.feedphases[7].pause_skimmer, true);
876                assert_eq!(c.feedphases[7].pause_main_pump_1, true);
877                assert_eq!(c.feedphases[7].pause_main_pump_2, true);
878                assert_eq!(c.feedphases[7].pause_aux_pump_1, true);
879                assert_eq!(c.feedphases[7].pause_aux_pump_2, true);
880                assert_eq!(c.feedphases[7].feed_duration, 15);
881                assert_eq!(c.feedphases[7].feed_skimmer, true);
882                assert_eq!(c.feedphases[7].feed_main_pump_1, true);
883                assert_eq!(c.feedphases[7].feed_main_pump_2, true);
884                assert_eq!(c.feedphases[7].feed_aux_pump_1, true);
885                assert_eq!(c.feedphases[7].feed_aux_pump_2, false);
886
887                assert_eq!(c.feedphases[8].pause_duration, 16);
888                assert_eq!(c.feedphases[8].pause_skimmer, true);
889                assert_eq!(c.feedphases[8].pause_main_pump_1, true);
890                assert_eq!(c.feedphases[8].pause_main_pump_2, true);
891                assert_eq!(c.feedphases[8].pause_aux_pump_1, false);
892                assert_eq!(c.feedphases[8].pause_aux_pump_2, false);
893                assert_eq!(c.feedphases[8].feed_duration, 17);
894                assert_eq!(c.feedphases[8].feed_skimmer, true);
895                assert_eq!(c.feedphases[8].feed_main_pump_1, true);
896                assert_eq!(c.feedphases[8].feed_main_pump_2, false);
897                assert_eq!(c.feedphases[8].feed_aux_pump_1, false);
898                assert_eq!(c.feedphases[8].feed_aux_pump_2, false);
899
900                assert_eq!(c.feedphases[9].pause_duration, 18);
901                assert_eq!(c.feedphases[9].pause_skimmer, true);
902                assert_eq!(c.feedphases[9].pause_main_pump_1, false);
903                assert_eq!(c.feedphases[9].pause_main_pump_2, false);
904                assert_eq!(c.feedphases[9].pause_aux_pump_1, false);
905                assert_eq!(c.feedphases[9].pause_aux_pump_2, false);
906                assert_eq!(c.feedphases[9].feed_duration, 19);
907                assert_eq!(c.feedphases[9].feed_skimmer, false);
908                assert_eq!(c.feedphases[9].feed_main_pump_1, false);
909                assert_eq!(c.feedphases[9].feed_main_pump_2, false);
910                assert_eq!(c.feedphases[9].feed_aux_pump_1, false);
911                assert_eq!(c.feedphases[9].feed_aux_pump_2, false);
912            }
913            Err(e) => {
914                panic!("Call to get_single_feedpattern_from_database has failed: {e:?}.");
915            }
916        }
917        println!("* checking retrieving of feed pattern succeeded.");
918
919        match sql_interface_feed.conn.exec_drop::<_, _>(
920            sql_query_string,
921            params! {
922            "profile_id" => 2, "profile_name" => "test2"
923            },
924        ) {
925            Ok(_) => {}
926            Err(e) => {
927                panic!("Error when inserting feed pattern into database: {e:?}");
928            }
929        };
930        let test_result: Result<SqlInterfaceFeed, SqlInterfaceError> =
931            SqlInterfaceFeed::new(sql_interface.get_connection().unwrap(), 1, 100, 0);
932        assert!(matches!(
933            test_result,
934            Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
935                _,
936                _,
937                2,
938                1
939            ))
940        ));
941        println!("* checking of feed pattern max rows limitation succeeded.");
942
943        // *****************************************************************************************
944    }
945
946    #[test]
947    // Test case includes all checks for this table combined in one function to limit the number of additional databases.
948    // Test case uses test database #34.
949    pub fn test_sql_interface_feedlog() {
950        let config: ConfigData = read_config_file_with_test_database(
951            "/config/aquarium_control_test_generic.toml".to_string(),
952            34,
953        );
954        println!("Testing with database {}", config.sql_interface.db_name);
955        let max_rows_feed_pattern = config.sql_interface.max_rows_feed_pattern;
956        let max_rows_feed_schedule = config.sql_interface.max_rows_feed_schedule;
957        let max_rows_feed_log = config.sql_interface.max_rows_feed_log;
958        let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
959            .expect("Initialization of SQL interface for test failed.");
960        let mut sql_interface_feed = SqlInterfaceFeed::new(
961            sql_interface.get_connection().unwrap(),
962            max_rows_feed_pattern,
963            max_rows_feed_schedule,
964            max_rows_feed_log,
965        )
966        .unwrap();
967
968        //*** check inserting of feed event into empty table ***************************************
969        match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDLOG.to_string()) {
970            Ok(_) => {}
971            Err(e) => panic!("Could not prepare test case: {e:?}"),
972        }
973        let feed_timestamp =
974            NaiveDateTime::parse_from_str("2022-04-11 11:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
975        match sql_interface_feed.insert_feed_event(feed_timestamp, 5.5, "Test".to_string(), 1) {
976            Ok(_) => {}
977            Err(e) => panic!("Could not insert feed event into database: {e:?}"),
978        }
979        // Read back feed log entry from the database
980        let single_feed_event_entry_array = match sql_interface_feed.conn.query_map(
981            sql_query_strings::SQL_QUERY_READ_LAST_FEED_EVENT,
982            |(timestamp, feeder_run_time, feed_profile_name, feed_profile_id)| SqlFeedEvent {
983                timestamp,
984                feeder_run_time,
985                feed_profile_name,
986                feed_profile_id,
987            },
988        ) {
989            Ok(c) => c,
990            Err(e) => {
991                panic!("Could not read feed event from data base: {e:?}");
992            }
993        };
994        // check if SQL response has the right structure
995        assert_eq!(single_feed_event_entry_array.len(), 1);
996        // check if SQL response contains the correct data
997        let feed_event = FeedEvent {
998            timestamp: NaiveDateTime::parse_from_str(
999                single_feed_event_entry_array[0].timestamp.as_str(),
1000                "%Y-%m-%d %H:%M:%S",
1001            )
1002            .expect("conversion of timestamp to NaiveDate failed."),
1003            feeder_run_time: single_feed_event_entry_array[0].feeder_run_time,
1004            feed_profile_name: single_feed_event_entry_array[0].feed_profile_name.clone(),
1005            feed_profile_id: single_feed_event_entry_array[0].feed_profile_id,
1006        };
1007        assert_eq!(feed_event.timestamp, feed_timestamp);
1008        assert_eq!(feed_event.feeder_run_time, 5.5);
1009        assert_eq!(feed_event.feed_profile_name, "Test");
1010        assert_eq!(feed_event.feed_profile_id, 1);
1011        println!("* checking inserting of feed event into log succeeded.");
1012        // *****************************************************************************************
1013
1014        // add one more entry to check max row check
1015        let feed_timestamp =
1016            NaiveDateTime::parse_from_str("2022-04-12 11:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
1017        match sql_interface_feed.insert_feed_event(feed_timestamp, 5.5, "Test2".to_string(), 2) {
1018            Ok(_) => {}
1019            Err(e) => panic!("Could not insert feed event into database: {e:?}"),
1020        }
1021        let test_result = SqlInterfaceFeed::new(sql_interface.get_connection().unwrap(), 0, 0, 1);
1022        assert!(matches!(
1023            test_result,
1024            Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
1025                _,
1026                _,
1027                _,
1028                _
1029            ))
1030        ));
1031    }
1032
1033    #[test]
1034    // Test case includes all checks for this table combined in one function to limit the number of additional databases.
1035    // Test case uses test database #35.
1036    pub fn test_sql_interface_feedschedule() {
1037        let config: ConfigData = read_config_file_with_test_database(
1038            "/config/aquarium_control_test_generic.toml".to_string(),
1039            35,
1040        );
1041        println!("Testing with database {}", config.sql_interface.db_name);
1042        let max_rows_feed_pattern = config.sql_interface.max_rows_feed_pattern;
1043        let max_rows_feed_schedule = config.sql_interface.max_rows_feed_schedule;
1044        let max_rows_feed_log = config.sql_interface.max_rows_feed_log;
1045        let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1046            .expect("Initialization of SQL interface for test failed.");
1047        let mut sql_interface_feed = SqlInterfaceFeed::new(
1048            sql_interface.get_connection().unwrap(),
1049            max_rows_feed_pattern,
1050            max_rows_feed_schedule,
1051            max_rows_feed_log,
1052        )
1053        .unwrap();
1054        // *** check trying to load feed schedule entry when table is empty *********************
1055        match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string()) {
1056            Ok(_) => {}
1057            Err(e) => panic!("Could not prepare test case: {e:?}"),
1058        }
1059        match sql_interface_feed.get_past_feedschedule_entries_from_database() {
1060            Ok(c) => {
1061                assert_eq!(c, None);
1062            }
1063            Err(_) => {
1064                panic!("Error when trying to load feed schedule entry from empty table.");
1065            }
1066        }
1067        println!("* reading from empty feed schedule (no result) succeeded.");
1068        // **************************************************************************************
1069
1070        // *** check trying to load feed schedule entry *****************************************
1071        let timestamp_reference = "2024-04-10 14:00:00";
1072        let profile_id_reference = 1;
1073        let profile_name_reference = "test";
1074        let repeat_daily_reference = true;
1075
1076        let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEED_SCHEDULE_ENTRY.to_string();
1077        match sql_interface_feed.conn.exec_drop::<_, _>(
1078            sql_query_string,
1079            params! {
1080            "timestamp" => timestamp_reference,
1081            "profile_id" => profile_id_reference,
1082            "profile_name" => profile_name_reference,
1083            "is_weekly" => false,
1084            "is_daily" => repeat_daily_reference,
1085            },
1086        ) {
1087            Ok(_) => {}
1088            Err(e) => {
1089                panic!("Error when inserting feed schedule entry into database: {e:?}");
1090            }
1091        };
1092
1093        let feedschedule_entry_before_update = match sql_interface_feed
1094            .get_past_feedschedule_entries_from_database()
1095        {
1096            Ok(feedschedule_entry_vector_opt) => match feedschedule_entry_vector_opt {
1097                Some(feedschedule_entry_vector) => {
1098                    if feedschedule_entry_vector.len() != 1 {
1099                        panic!(
1100                            "expected to find one feed schedule entry in data base, found {}",
1101                            feedschedule_entry_vector.len()
1102                        )
1103                    }
1104                    let feedschedule_entry = feedschedule_entry_vector.get(0).cloned().unwrap();
1105                    assert_eq!(
1106                        feedschedule_entry.timestamp,
1107                        NaiveDateTime::parse_from_str(timestamp_reference, "%Y-%m-%d %H:%M:%S")
1108                            .unwrap()
1109                    );
1110                    println!(
1111                        "timestamp read from the database: {}",
1112                        feedschedule_entry.timestamp
1113                    );
1114                    assert_eq!(feedschedule_entry.profile_id, profile_id_reference);
1115                    assert_eq!(feedschedule_entry.profile_name, profile_name_reference);
1116                    assert_eq!(feedschedule_entry.repeat_daily, repeat_daily_reference);
1117                    feedschedule_entry
1118                }
1119                None => {
1120                    panic!("SQL database returned empty result although feed schedule entry was inserted.");
1121                }
1122            },
1123            Err(_) => {
1124                panic!("Error occurred when trying to load feed schedule entry.");
1125            }
1126        };
1127        println!("* reading existing entry from feed schedule succeeded.");
1128        // *****************************************************************************************
1129
1130        // *** check updating feed schedule with repetition ****************************************
1131        println!(
1132            "Feed schedule entry before database update: {}",
1133            feedschedule_entry_before_update
1134        );
1135
1136        // calculate dynamic reference
1137        let local_date_tomorrow: NaiveDate =
1138            (Local::now().naive_local() + Duration::days(1)).into();
1139        let reference_timestamp_tomorrow_same_time = NaiveDateTime::new(
1140            local_date_tomorrow,
1141            feedschedule_entry_before_update.timestamp.time(),
1142        );
1143
1144        let mut feedschedule_entries_before_update: Vec<FeedScheduleEntry> = Vec::new();
1145        feedschedule_entries_before_update.push(feedschedule_entry_before_update);
1146
1147        match sql_interface_feed
1148            .update_feedschedule_entries_in_database(&mut feedschedule_entries_before_update)
1149        {
1150            Ok(()) => {}
1151            Err(e) => {
1152                panic!("Error occurred when trying to update feedschedule entry which is marked for repetition: {e:?}");
1153            }
1154        }
1155
1156        let mut feedschedule_entry_after_update = match sql_interface_feed
1157            .get_future_feedschedule_entry_from_database()
1158        {
1159            Ok(c) => match c {
1160                Some(d) => {
1161                    assert_eq!(d.timestamp, reference_timestamp_tomorrow_same_time);
1162                    assert_eq!(d.profile_id, profile_id_reference);
1163                    assert_eq!(d.profile_name, profile_name_reference);
1164                    assert_eq!(d.repeat_daily, repeat_daily_reference);
1165                    d
1166                }
1167                None => {
1168                    panic!("SQL database returned empty result although feed schedule entry was inserted.");
1169                }
1170            },
1171            Err(_) => {
1172                panic!("Error occurred when trying to load feed schedule entry.");
1173            }
1174        };
1175        println!(
1176            "Feed schedule entry after database update: {}",
1177            feedschedule_entry_after_update
1178        );
1179        println!("* update existing entry from feed schedule succeeded.");
1180        // *****************************************************************************************
1181
1182        // *** test updating feed schedule without repetition **************************************
1183        // modify existing feed schedule entry -> it shall be deleted with a call to update
1184        feedschedule_entry_after_update.repeat_daily = false;
1185        let mut feedschedule_entries_after_update: Vec<FeedScheduleEntry> = Vec::new();
1186        feedschedule_entries_after_update.push(feedschedule_entry_after_update);
1187
1188        match sql_interface_feed
1189            .update_feedschedule_entries_in_database(&mut feedschedule_entries_after_update)
1190        {
1191            Ok(()) => {}
1192            Err(e) => {
1193                panic!("Error occurred when trying to update feedschedule entry which is not marked for repetition: {e:?}");
1194            }
1195        }
1196
1197        match sql_interface_feed.get_past_feedschedule_entries_from_database() {
1198            Ok(c) => {
1199                assert_eq!(c, None);
1200            }
1201            Err(_) => {
1202                panic!("Error when trying to load feed schedule entry from empty table.");
1203            }
1204        };
1205        println!("* deleting existing entry from feed schedule succeeded.");
1206        // *****************************************************************************************
1207
1208        // *** check updating multiple feed schedule entries with repetition ***********************
1209        // calculate dynamic reference
1210        match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_FEEDSCHEDULE.to_string()) {
1211            Ok(_) => {}
1212            Err(e) => panic!("Could not prepare test case: {e:?}"),
1213        }
1214
1215        let timestamp_0 = "2024-04-10 14:00:00";
1216        let profile_id_0 = 0;
1217        let profile_name_0 = "test0";
1218        let repeat_daily_0 = true;
1219
1220        let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEED_SCHEDULE_ENTRY.to_string();
1221        match sql_interface_feed.conn.exec_drop::<_, _>(
1222            sql_query_string,
1223            params! {
1224                "timestamp" => timestamp_0,
1225                "profile_id" => profile_id_0,
1226                "profile_name" => profile_name_0,
1227                "is_weekly" => false,
1228                "is_daily" => repeat_daily_0,
1229            },
1230        ) {
1231            Ok(_) => {}
1232            Err(e) => {
1233                panic!("Error when inserting feed schedule entry into database: {e:?}");
1234            }
1235        };
1236
1237        let timestamp_1 = "2024-04-10 15:00:00";
1238        let profile_id_1 = 1;
1239        let profile_name_1 = "test1";
1240        let repeat_daily_1 = true;
1241
1242        let sql_query_string = sql_query_strings::SQL_QUERY_WRITE_FEED_SCHEDULE_ENTRY.to_string();
1243        match sql_interface_feed.conn.exec_drop::<_, _>(
1244            sql_query_string,
1245            params! {
1246                "timestamp" => timestamp_1,
1247                "profile_id" => profile_id_1,
1248                "profile_name" => profile_name_1,
1249                "is_weekly" => false,
1250                "is_daily" => repeat_daily_1,
1251            },
1252        ) {
1253            Ok(_) => {}
1254            Err(e) => {
1255                panic!("Error when inserting feed schedule entry into database: {e:?}");
1256            }
1257        };
1258
1259        let mut feedschedule_entries_before_update = match sql_interface_feed
1260            .get_past_feedschedule_entries_from_database()
1261        {
1262            Ok(feedschedule_entry_vector_opt) => match feedschedule_entry_vector_opt {
1263                Some(feedschedule_entry_vector) => feedschedule_entry_vector,
1264                None => {
1265                    panic!("SQL database returned empty result although feed schedule entries were inserted.");
1266                }
1267            },
1268            Err(_) => {
1269                panic!("Error occurred when trying to load feed schedule entry.");
1270            }
1271        };
1272
1273        let local_date_tomorrow: NaiveDate =
1274            (Local::now().naive_local() + Duration::days(1)).into();
1275
1276        let reference_timestamp0_tomorrow_same_time = NaiveDateTime::new(
1277            local_date_tomorrow,
1278            feedschedule_entries_before_update
1279                .get(1)
1280                .unwrap()
1281                .timestamp
1282                .time(),
1283        );
1284        let reference_timestamp1_tomorrow_same_time = NaiveDateTime::new(
1285            local_date_tomorrow,
1286            feedschedule_entries_before_update
1287                .get(0)
1288                .unwrap()
1289                .timestamp
1290                .time(),
1291        );
1292
1293        match sql_interface_feed
1294            .update_feedschedule_entries_in_database(&mut feedschedule_entries_before_update)
1295        {
1296            Ok(()) => {}
1297            Err(e) => {
1298                panic!("Error occurred when trying to update feedschedule entry which is marked for repetition: {e:?}");
1299            }
1300        }
1301
1302        match sql_interface_feed.get_future_feedschedule_entries_from_database() {
1303            Ok(c) => match c {
1304                Some(d) => {
1305                    assert_eq!(d.len(), 2);
1306                    assert_eq!(
1307                        d.get(0).unwrap().timestamp,
1308                        reference_timestamp0_tomorrow_same_time
1309                    );
1310                    assert_eq!(d.get(0).unwrap().profile_id, profile_id_0);
1311                    assert_eq!(d.get(0).unwrap().profile_name, profile_name_0);
1312                    assert_eq!(d.get(0).unwrap().repeat_daily, repeat_daily_0);
1313                    assert_eq!(
1314                        d.get(1).unwrap().timestamp,
1315                        reference_timestamp1_tomorrow_same_time
1316                    );
1317                    assert_eq!(d.get(1).unwrap().profile_id, profile_id_1);
1318                    assert_eq!(d.get(1).unwrap().profile_name, profile_name_1);
1319                    assert_eq!(d.get(1).unwrap().repeat_daily, repeat_daily_1);
1320                }
1321                None => {
1322                    panic!("SQL database returned empty result although feed schedule entry was inserted.");
1323                }
1324            },
1325            Err(_) => {
1326                panic!("Error occurred when trying to load feed schedule entry.");
1327            }
1328        };
1329        println!("* update of existing entries from feed schedule succeeded.");
1330        // **************************************************************************************
1331
1332        let test_result: Result<SqlInterfaceFeed, SqlInterfaceError> =
1333            SqlInterfaceFeed::new(sql_interface.get_connection().unwrap(), 100, 1, 0);
1334        assert!(matches!(
1335            test_result,
1336            Err(SqlInterfaceError::DatabaseFeedTableContainsTooManyRows(
1337                _,
1338                _,
1339                2,
1340                1
1341            ))
1342        ));
1343        println!("* checking of feed schedule max rows limitation succeeded.");
1344    }
1345}