aquarium_control/database/
sql_interface_refill.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 water refill system.
11//!
12//! This module provides a dedicated interface, `SqlInterfaceRefill`, for handling
13//! all SQL operations related to the refill event log (`refill` table). It also
14//! defines a `DatabaseInterfaceRefillTrait` to decouple the core refill logic
15//! from the specific database implementation, adhering to the Dependency Inversion Principle.
16//!
17//! ## Key Components
18//!
19//! - **`DatabaseInterfaceRefillTrait`**: An abstraction that defines the contract for
20//!   any component that interacts with refill data. This is crucial for testability,
21//!   as it allows for mock implementations to be used in unit tests without needing a
22//!   live database.
23//!
24//! - **`SqlInterfaceRefill` Struct**: The concrete implementation of the trait, holding
25//!   a database connection and providing methods for all refill-related operations.
26//!
27//! - **`new()` Constructor**: A critical entry point that performs "fail-fast"
28//!   validation at initialization. It checks whether the number of rows in the
29//!   `refill` table exceeds the `max_rows_refill` limit defined in the application
30//!   configuration, preventing uncontrolled table growth.
31//!
32//! - **Statistics Methods**: Functions like `get_refill_count_of_last_24h` and
33//!   `get_refill_volume_of_last_24h` provide aggregated data about recent refill
34//!   activity.
35//!
36//! - **`insert_refill_event()`**: The core logging function that persists the details
37//!   of a completed refill operation to the database.
38//!
39//! ## Design and Purpose
40//!
41//! The main goal of this module is to provide a robust, encapsulated, and testable
42//! interface for the refill subsystem's data.
43//!
44//! - **Encapsulation**: All SQL queries and logic specific to the `refill` table are
45//!   contained within this module, separating concerns.
46//!
47//! - **Data Integrity**: The constructor's validation logic enforces the configured
48//!   row limit at startup, ensuring the system doesn't start with an overgrown log table.
49//!
50//! - **Abstraction for Testability**: The `DatabaseInterfaceRefillTrait` allows higher-level
51//!   components to be tested independently of the database.
52
53use chrono::*;
54use mysql::prelude::*;
55use mysql::*;
56
57use crate::database::pingable::Pingable;
58use crate::database::sql_query_strings::SQL_QUERY_CHECK_REFILL_COUNT;
59use crate::database::{
60    sql_interface::SqlInterface, sql_interface_error::SqlInterfaceError, sql_query_strings,
61};
62
63/// Trait for interfacing between refill and SQL database
64/// This trait is prerequisite to use mock implementation for testing
65pub trait DatabaseInterfaceRefillTrait: Pingable {
66    fn get_duration_since_last_refill(&mut self) -> Result<u64, SqlInterfaceError>;
67
68    fn get_positive_integer_value_from_database(
69        &mut self,
70        sql_query_string: &str,
71    ) -> Result<u64, SqlInterfaceError>;
72
73    fn check_empty(&mut self) -> Result<u64, SqlInterfaceError>;
74
75    fn get_refill_count_of_last_hour(&mut self) -> Result<u64, SqlInterfaceError>;
76
77    fn get_refill_count_of_last_24h(&mut self) -> Result<u64, SqlInterfaceError>;
78
79    fn get_refill_volume_of_last_24h(&mut self) -> Result<f64, SqlInterfaceError>;
80
81    fn get_refill_volume_of_last_hour(&mut self) -> Result<f64, SqlInterfaceError>;
82
83    fn insert_refill_event(
84        &mut self,
85        timestamp: NaiveDateTime,
86        duration: f64,
87        volume: f64,
88        error_code: i32,
89    ) -> Result<(), SqlInterfaceError>;
90}
91
92/// Contains the configuration and the implementation of the SQL interface for Refill.
93#[derive(Debug)]
94pub struct SqlInterfaceRefill {
95    /// Connection to the database
96    pub conn: PooledConn,
97}
98
99impl SqlInterfaceRefill {
100    /// Creates a new `SqlInterfaceRefill` instance.
101    ///
102    /// This constructor initializes the refill management SQL interface with an established
103    /// database connection. It performs a pre-flight check to ensure the number of rows
104    /// in the `refill` table does not exceed the configured limit.
105    ///
106    /// # Arguments
107    /// * `conn` - An active, pooled database connection.
108    /// * `max_rows_refill` - The maximum allowed number of rows in the `refill` table.
109    ///   If `0`, the check is skipped.
110    ///
111    /// # Returns
112    /// A `Result` containing a new `SqlInterfaceRefill` instance on success.
113    ///
114    /// # Errors
115    /// This function will return an error if:
116    /// - The initial database query to get the table row count fails (`DatabaseCheckRefillFailure`).
117    /// - The retrieved row count is a negative number, indicating a database issue (`DatabaseRefillTableNegativeValue`).
118    /// - The number of rows in the `refill` table exceeds the configured `max_rows_refill` limit (`DatabaseRefillTableContainsTooManyRows`).
119    pub fn new(
120        mut conn: PooledConn,
121        max_rows_refill: u64,
122    ) -> Result<SqlInterfaceRefill, SqlInterfaceError> {
123        let count_rows =
124            SqlInterface::get_single_integer_from_database(&mut conn, SQL_QUERY_CHECK_REFILL_COUNT)
125                .map_err(|e| SqlInterfaceError::DatabaseCheckRefillFailure {
126                    location: module_path!().to_string(),
127                    source: Box::new(e),
128                })?;
129
130        // check the query results
131        if count_rows < 0 {
132            return Err(SqlInterfaceError::DatabaseRefillTableNegativeValue(
133                module_path!().to_string(),
134                count_rows,
135            ));
136        }
137
138        if max_rows_refill > 0 {
139            // execute the check only when the limit is greater than zero
140            if count_rows > max_rows_refill.cast_signed() {
141                return Err(SqlInterfaceError::DatabaseRefillTableContainsTooManyRows(
142                    module_path!().to_string(),
143                    count_rows.cast_unsigned(),
144                    max_rows_refill,
145                ));
146            }
147        }
148
149        Ok(SqlInterfaceRefill { conn })
150    }
151
152    /// Fetches the timestamp of the most recent water refill event from the database.
153    ///
154    /// This private helper function queries the database for the latest recorded
155    /// refill event.
156    ///
157    /// # Returns
158    /// A `Result` containing the `NaiveDateTime` of the last refill event on success.
159    ///
160    /// # Errors
161    /// This function will return an `Err` variant of `SqlInterfaceError`, most likely
162    /// `TimestampRequestFailure`, if:
163    /// - The underlying database query fails due to a connection issue or invalid SQL.
164    /// - The `refill` table is empty, and thus no timestamp can be returned.
165    /// - The query unexpectedly returns more than one row.
166    fn get_last_refill_timestamp(&mut self) -> Result<NaiveDateTime, SqlInterfaceError> {
167        SqlInterface::get_timestamp(
168            &mut self.conn,
169            sql_query_strings::SQL_QUERY_READ_LAST_REFILL_TIMESTAMP,
170        )
171    }
172}
173
174impl DatabaseInterfaceRefillTrait for SqlInterfaceRefill {
175    /// Calculates the time elapsed in seconds since the most recent water refill event.
176    ///
177    /// This function determines the duration by comparing the current database timestamp
178    /// with the timestamp of the last recorded refill event.
179    ///
180    /// # Returns
181    /// A `Result` containing the number of seconds (`u64`) that have passed since the last refill event.
182    ///
183    /// # Errors
184    /// This function will return an error if:
185    /// - It fails to retrieve either the current timestamp or the last refill timestamp from the database.
186    ///   The original error from the database call will be propagated.
187    /// - The calculated duration is negative (`NegativeTimeDeltaSeconds`), which suggests a data anomaly
188    ///   where the "last refill" timestamp is in the future.
189    fn get_duration_since_last_refill(&mut self) -> Result<u64, SqlInterfaceError> {
190        let current_datetime = SqlInterface::get_current_timestamp(&mut self.conn)?;
191
192        let last_refill_datetime = self.get_last_refill_timestamp()?;
193
194        let delta_time = current_datetime.signed_duration_since(last_refill_datetime);
195
196        if delta_time.num_seconds() >= 0 {
197            Ok(delta_time.num_seconds() as u64)
198        } else {
199            Err(SqlInterfaceError::NegativeTimeDeltaSeconds(
200                module_path!().to_string(),
201                delta_time.num_seconds(),
202            ))
203        }
204    }
205
206    /// Executes an SQL query to retrieve a single integer, validating it as non-negative.
207    ///
208    /// This helper function is used for database queries that are expected to return
209    /// a single, non-negative integer.
210    ///
211    /// # Arguments
212    /// * `sql_query_string` - The SQL query to be executed.
213    ///
214    /// # Returns
215    /// A `Result` containing the retrieved integer value as a `u64` on success.
216    ///
217    /// # Errors
218    /// This function will return an error if:
219    /// - The underlying database call fails (e.g., query execution error, no result, or multiple results).
220    ///   The original error is propagated.
221    /// - The database query returns a negative integer (`NegativeInteger`), which is not expected.
222    fn get_positive_integer_value_from_database(
223        &mut self,
224        sql_query_string: &str,
225    ) -> Result<u64, SqlInterfaceError> {
226        match SqlInterface::get_single_integer_from_database(&mut self.conn, sql_query_string) {
227            Ok(c) => {
228                if c >= 0 {
229                    Ok(c as u64)
230                } else {
231                    Err(SqlInterfaceError::NegativeInteger(
232                        module_path!().to_string(),
233                        c,
234                    ))
235                }
236            }
237            Err(e) => Err(e),
238        }
239    }
240
241    /// Checks whether the refill events table in the database is empty.
242    ///
243    /// This function executes an SQL query to determine if any entries exist
244    /// in the refill log.
245    ///
246    /// # Returns
247    /// A `Result` which is:
248    /// - `Ok(0)` if the refill table contains no entries.
249    /// - `Ok(1)` if the refill table contains at least one entry.
250    ///
251    /// # Errors
252    /// This function will return an `Err` if the database query fails or if the query
253    /// returns an unexpected value (e.g., a negative number).
254    fn check_empty(&mut self) -> Result<u64, SqlInterfaceError> {
255        self.get_positive_integer_value_from_database(
256            sql_query_strings::SQL_QUERY_CHECK_REFILL_EMPTY,
257        )
258    }
259
260    /// Retrieves the total count of water refill events that occurred within the last hour.
261    ///
262    /// This function queries the database to count all refill events whose timestamps
263    /// fall within the most recent 60-minute period.
264    ///
265    /// # Returns
266    /// A `Result` containing the number of refill events (`u64`) recorded in the last hour.
267    ///
268    /// # Errors
269    /// Returns `SqlInterfaceError::RefillPastDataRetrievalFailed` if the underlying
270    /// database call fails. This can be caused by a connection issue, an invalid query,
271    /// or if the query returns a negative number where a positive count is expected.
272    fn get_refill_count_of_last_hour(&mut self) -> Result<u64, SqlInterfaceError> {
273        self.get_positive_integer_value_from_database(
274            sql_query_strings::SQL_QUERY_READ_REFILL_COUNT_LAST_HOUR,
275        )
276        .map_err(|e| SqlInterfaceError::RefillPastDataRetrievalFailed {
277            location: module_path!().to_string(),
278            category: "count".to_string(),
279            interval: "last hour".to_string(),
280            source: Box::new(e),
281        })
282    }
283
284    /// Retrieves the total count of water refill events that occurred within the last 24 hours.
285    ///
286    /// This function queries the database to count all refill events whose timestamps
287    /// fall within the most recent 24-hour period.
288    ///
289    /// # Returns
290    /// A `Result` containing the number of refill events (`u64`) recorded in the last 24 hours.
291    ///
292    /// # Errors
293    /// Returns `SqlInterfaceError::RefillPastDataRetrievalFailed` if the underlying
294    /// database call fails. This can be caused by a connection issue, an invalid query,
295    /// or if the query returns a negative number where a positive count is expected.
296    fn get_refill_count_of_last_24h(&mut self) -> Result<u64, SqlInterfaceError> {
297        self.get_positive_integer_value_from_database(
298            sql_query_strings::SQL_QUERY_READ_REFILL_COUNT_LAST_24H,
299        )
300        .map_err(|e| SqlInterfaceError::RefillPastDataRetrievalFailed {
301            location: module_path!().to_string(),
302            category: "count".to_string(),
303            interval: "last 24h".to_string(),
304            source: Box::new(e),
305        })
306    }
307
308    /// Retrieves the total volume of water (in liters) refilled within the last 24 hours.
309    ///
310    /// This function queries the database to sum the volume of all refill events
311    /// that occurred within the most recent 24-hour period.
312    ///
313    /// # Returns
314    /// A `Result` containing the total volume (`f64`) of water refilled in the last 24 hours.
315    ///
316    /// # Errors
317    /// Returns `SqlInterfaceError::RefillPastDataRetrievalFailed` if the underlying
318    /// database call fails. This can be caused by a connection issue, an invalid query,
319    /// or if the query does not return a single float value.
320    fn get_refill_volume_of_last_24h(&mut self) -> Result<f64, SqlInterfaceError> {
321        SqlInterface::get_single_float_from_database(
322            &mut self.conn,
323            sql_query_strings::SQL_QUERY_READ_REFILL_VOLUME_LAST_24H,
324        )
325        .map_err(|e| SqlInterfaceError::RefillPastDataRetrievalFailed {
326            location: module_path!().to_string(),
327            category: "volume".to_string(),
328            interval: "last 24h".to_string(),
329            source: Box::new(e),
330        })
331    }
332
333    /// Retrieves the total volume of water (in liters) refilled within the last hour.
334    ///
335    /// This function queries the database to sum the volume of all refill events
336    /// that occurred within the most recent 60-minute period.
337    ///
338    /// # Returns
339    /// A `Result` containing the total volume (`f64`) of water refilled in the last hour.
340    ///
341    /// # Errors
342    /// Returns `SqlInterfaceError::RefillPastDataRetrievalFailed` if the underlying
343    /// database call fails. This can be caused by a connection issue, an invalid query,
344    /// or if the query does not return a single float value.
345    fn get_refill_volume_of_last_hour(&mut self) -> Result<f64, SqlInterfaceError> {
346        SqlInterface::get_single_float_from_database(
347            &mut self.conn,
348            sql_query_strings::SQL_QUERY_READ_REFILL_VOLUME_LAST_HOUR,
349        )
350        .map_err(|e| SqlInterfaceError::RefillPastDataRetrievalFailed {
351            location: module_path!().to_string(),
352            category: "volume".to_string(),
353            interval: "last hour".to_string(),
354            source: Box::new(e),
355        })
356    }
357
358    /// Inserts a new water refill event record into the database.
359    ///
360    /// This function logs the details of a completed water refill operation,
361    /// including its timestamp, duration, volume, and any associated error code.
362    ///
363    /// # Arguments
364    /// * `timestamp` - The `NaiveDateTime` when the refill event occurred.
365    /// * `duration` - The duration (in seconds) of the refill event.
366    /// * `volume` - The volume (in liters) of water dispensed during the refill.
367    /// * `error_code` - A numerical code associated with the event (0 for success).
368    ///
369    /// # Returns
370    /// An empty `Result` (`Ok(())`) if the refill event record was successfully inserted.
371    ///
372    /// # Errors
373    /// Returns `SqlInterfaceError::InsertRefillEventFailure` if the `INSERT` query fails.
374    /// This can be caused by a lost connection, constraint violations, or incorrect
375    /// data types. The original `mysql::Error` is included as the source.
376    fn insert_refill_event(
377        &mut self,
378        timestamp: NaiveDateTime,
379        duration: f64,
380        volume: f64,
381        error_code: i32,
382    ) -> Result<(), SqlInterfaceError> {
383        self.conn
384            .exec_drop::<_, _>(
385                sql_query_strings::SQL_QUERY_WRITE_REFILL_EVENT,
386                params! {
387                "timestamp" => timestamp,
388                "duration" => duration,
389                "volume" => volume,
390                "error_code" => error_code,
391                },
392            )
393            .map_err(|e| SqlInterfaceError::InsertRefillEventFailure {
394                location: module_path!().to_string(),
395                query: sql_query_strings::SQL_QUERY_WRITE_REFILL_EVENT.to_string(),
396                source: e,
397            })
398    }
399}
400
401#[cfg(test)]
402pub mod tests {
403    use chrono::{Local, NaiveDateTime, SubsecRound};
404    use mysql::prelude::Queryable;
405
406    use crate::database::sql_interface_refill::{DatabaseInterfaceRefillTrait, SqlInterfaceRefill};
407    use crate::database::sql_query_strings::SQL_TABLE_REFILL;
408    use crate::database::{
409        sql_interface::SqlInterface, sql_interface_error::SqlInterfaceError, sql_query_strings,
410    };
411    use crate::utilities::config::{read_config_file_with_test_database, ConfigData};
412
413    pub struct SqlRefillEvent {
414        timestamp: String,
415        duration: f32,
416        volume: f32,
417        error_code: i32,
418    }
419
420    pub struct RefillEvent {
421        timestamp: NaiveDateTime,
422        duration: f32,
423        volume: f32,
424        error_code: i32,
425    }
426    #[test]
427    // This test case verifies the validation logic within the `SqlInterfaceRefill::new()` constructor.
428    // It covers the following scenarios:
429    // 1. Happy Path: Initialization succeeds when the row count is within the limit.
430    // 2. Failure on Row Limit: Fails when the `refill` table exceeds its max row count.
431    // 3. Deactivated Check: Succeeds even with excess rows if the limit is set to 0.
432    // 4. Edge Case: Succeeds when the row count is exactly the limit.
433    // Test case uses a dedicated test database to avoid conflicts.
434    fn test_sql_interface_refill_new() {
435        // --- Common Setup ---
436        let config: ConfigData = read_config_file_with_test_database(
437            "/config/aquarium_control_test_generic.toml".to_string(),
438            62,
439        );
440        println!("Testing with database {}", config.sql_interface.db_name);
441        let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
442            .expect("Initialization of SQL interface for test failed.");
443
444        // --- Test Case 1: Happy Path (the row count is within limit) ---
445        println!("* Testing new() with valid row count (Happy Path)...");
446        SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_REFILL.to_string())
447            .expect("Test setup failed: Could not truncate table.");
448
449        // Insert one row
450        let mut refill_db = SqlInterfaceRefill {
451            conn: sql_interface.get_connection().unwrap(),
452        };
453        refill_db
454            .insert_refill_event(Local::now().naive_local(), 1.0, 1.0, 0)
455            .expect("Test setup failed: Could not insert row.");
456
457        // Check: new() should succeed with a generous limit of 10
458        let result = SqlInterfaceRefill::new(sql_interface.get_connection().unwrap(), 10);
459        assert!(
460            result.is_ok(),
461            "Expected new() to succeed on happy path, but it failed: {:?}",
462            result.err()
463        );
464        println!("* Succeeded: Happy path initialization is successful.");
465
466        // --- Test Case 2: Failure on row limit exceeded ---
467        println!("* Testing new() with too many rows in the refill table...");
468        // Insert a second row
469        let mut refill_db = SqlInterfaceRefill {
470            conn: sql_interface.get_connection().unwrap(),
471        };
472        refill_db
473            .insert_refill_event(Local::now().naive_local(), 2.0, 2.0, 0)
474            .expect("Test setup failed: Could not insert second row.");
475
476        // Check: new() should fail with a strict limit of 1
477        let result = SqlInterfaceRefill::new(sql_interface.get_connection().unwrap(), 1);
478        assert!(
479            matches!(
480                result,
481                Err(SqlInterfaceError::DatabaseRefillTableContainsTooManyRows(
482                    _,
483                    _,
484                    _
485                ))
486            ),
487            "Expected row limit error, but got {:?}",
488            result
489        );
490        println!("* Succeeded: Initialization fails if refill table exceeds row limit.");
491
492        // --- Test Case 3: Deactivated check (limit is 0) ---
493        println!("* Testing new() with deactivated row limit check (limit = 0)...");
494        // We reuse the state from the previous test (2 rows in the table)
495        // Set the limit to 0
496        let result = SqlInterfaceRefill::new(sql_interface.get_connection().unwrap(), 0);
497        assert!(
498            result.is_ok(),
499            "Expected new() to succeed with deactivated check, but it failed: {:?}",
500            result.err()
501        );
502        println!("* Succeeded: Initialization passes when row limit check is deactivated.");
503
504        // --- Test Case 4: Edge case (row count equals limit) ---
505        println!("* Testing new() with row count equal to the limit...");
506        // We reuse the state from the previous test (2 rows in the table)
507        // Set the limit to 2
508        let result = SqlInterfaceRefill::new(sql_interface.get_connection().unwrap(), 2);
509        assert!(
510            result.is_ok(),
511            "Expected new() to succeed when row count equals limit, but it failed: {:?}",
512            result.err()
513        );
514        println!("* Succeeded: Initialization passes when row count is exactly the limit.");
515    }
516
517    #[test]
518    // Test case includes all checks for this table combined in one function to limit the number of additional databases.
519    // Test case uses test database #37.
520    pub fn test_sql_interface_refill() {
521        let config: ConfigData = read_config_file_with_test_database(
522            "/config/aquarium_control_test_generic.toml".to_string(),
523            37,
524        );
525        println!("Testing with database {}", config.sql_interface.db_name);
526        let max_rows_refill = config.sql_interface.max_rows_refill;
527        let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
528            .expect("Initialization of SQL interface for test failed.");
529        let mut sql_interface_refill =
530            SqlInterfaceRefill::new(sql_interface.get_connection().unwrap(), max_rows_refill)
531                .unwrap();
532
533        // *** check timestamp of last refill *************************************************
534        match SqlInterface::truncate_table(&mut sql_interface, SQL_TABLE_REFILL.to_string()) {
535            Ok(_) => {}
536            Err(e) => {
537                panic!("Could not prepare test case: {e:?}")
538            }
539        }
540        assert!(matches!(sql_interface_refill.check_empty(), Ok(_)));
541        assert!(matches!(
542            sql_interface_refill.get_last_refill_timestamp(),
543            Err(SqlInterfaceError::TimestampRequestFailure {
544                location: _,
545                query: _
546            })
547        ));
548        println!("* checking timestamp of last refill (FAIL) succeeded.");
549        // **************************************************************************************
550
551        // *** check timestamp of last refill - ****************************************************
552        let refill_timestamp =
553            NaiveDateTime::parse_from_str("2025-04-12 07:00:00", "%Y-%m-%d %H:%M:%S").unwrap();
554        match sql_interface_refill.insert_refill_event(refill_timestamp, 3.6, 0.4, 0) {
555            Ok(_) => {}
556            Err(e) => panic!("Could not insert refill event into database: {e:?}"),
557        };
558        assert!(matches!(sql_interface_refill.check_empty(), Ok(1)));
559        assert!(matches!(
560            sql_interface_refill.get_last_refill_timestamp(),
561            Ok(_)
562        ));
563        println!("* checking timestamp of last refill succeeded.");
564        //**************************************************************************************
565
566        //*** check duration since last refill - **************************************************
567        let duration_since_last_refill = match sql_interface_refill.get_duration_since_last_refill()
568        {
569            Ok(c) => c,
570            Err(e) => panic!("Could not calculate duration since last refill: {e:?}"),
571        };
572        let reference_duration_since_last_refill: u64 =
573            SqlInterface::get_duration_from_naive_timestamp_to_local_now(refill_timestamp)
574                .unwrap()
575                .num_seconds() as u64;
576        assert_eq!(
577            duration_since_last_refill,
578            reference_duration_since_last_refill
579        );
580        println!("* checking duration since last refill succeeded.");
581        //**************************************************************************************
582
583        // *** check refill count of last 24h (zero) - *********************************************
584        let refill_count_of_last_24h_zero =
585            match sql_interface_refill.get_refill_count_of_last_24h() {
586                Ok(c) => c,
587                Err(e) => panic!("Could not get refill count from last 24h (target=0): {e:?}"),
588            };
589        assert_eq!(refill_count_of_last_24h_zero, 0);
590        println!("* checking refill count of last 24h (target zero) succeeded.");
591        // **************************************************************************************
592
593        // *** check refill count of last hour (zero) - ********************************************
594        let refill_count_of_last_hour_zero =
595            match sql_interface_refill.get_refill_count_of_last_hour() {
596                Ok(c) => c,
597                Err(e) => panic!("Could not get refill count from last hour (target=0): {e:?}"),
598            };
599        assert_eq!(refill_count_of_last_hour_zero, 0);
600        println!("* checking refill count of last hour (target zero) succeeded.");
601        // **************************************************************************************
602
603        //*** check refill volume of last 24h (zero) - *********************************************
604        let refill_volume_of_last_24h_zero =
605            match sql_interface_refill.get_refill_volume_of_last_24h() {
606                Ok(c) => c,
607                Err(e) => panic!("Could not get refill volume from last 24h (target=0): {e:?}"),
608            };
609        assert_eq!(refill_volume_of_last_24h_zero, 0.0);
610        println!("* checking refill volume of last 24h (target zero) succeeded.");
611        // **************************************************************************************
612
613        //*** check refill volume of last hour (zero) - *********************************************
614        let refill_volume_of_last_hour_zero =
615            match sql_interface_refill.get_refill_volume_of_last_hour() {
616                Ok(c) => c,
617                Err(e) => panic!("Could not get refill volume from last hour (target=0): {e:?}"),
618            };
619        assert_eq!(refill_volume_of_last_hour_zero, 0.0);
620        println!("* checking refill volume of last hour (target zero) succeeded.");
621        // **************************************************************************************
622
623        // *** check refill count of last 24h (one) - *********************************************
624        let naive_datetime_12h_ago = SqlInterface::get_naive_timestamp_from_past(12, 0);
625        match sql_interface_refill.insert_refill_event(naive_datetime_12h_ago, 5.0, 0.25, -2) {
626            Ok(_) => {}
627            Err(e) => {
628                panic!("Could not insert refill event in data base for test preparation: {e:?}")
629            }
630        }
631        let refill_count_of_last_24h_one = match sql_interface_refill.get_refill_count_of_last_24h()
632        {
633            Ok(c) => c,
634            Err(e) => panic!("Could not get refill count from last 24h (target=1): {e:?}"),
635        };
636        assert_eq!(refill_count_of_last_24h_one, 1);
637        println!("* checking refill count of last 24h (target non-zero) succeeded.");
638        // **************************************************************************************
639
640        // *** check refill volume of last 24h (one) - *********************************************
641        let refill_volume_of_last_24h_one =
642            match sql_interface_refill.get_refill_volume_of_last_24h() {
643                Ok(c) => c,
644                Err(e) => panic!("Could not get refill volume from last 24h (target=0.25): {e:?}"),
645            };
646        assert_eq!(refill_volume_of_last_24h_one, 0.25);
647        println!("* checking refill volume (target non-zero) succeeded.");
648        // **************************************************************************************
649
650        // *** check refill count of last hour (one) - *********************************************
651        let naive_datetime_30min_ago = SqlInterface::get_naive_timestamp_from_past(0, 30);
652        match sql_interface_refill.insert_refill_event(naive_datetime_30min_ago, 5.0, 0.25, -2) {
653            Ok(_) => {}
654            Err(e) => {
655                panic!("Could not insert refill event in data base for test preparation: {e:?}")
656            }
657        }
658        let refill_count_of_last_hour_one =
659            match sql_interface_refill.get_refill_count_of_last_hour() {
660                Ok(c) => c,
661                Err(e) => panic!("Could not get refill count from last hour (target=1): {e:?}"),
662            };
663        assert_eq!(refill_count_of_last_hour_one, 1);
664        println!("* checking refill count of last hour (target non-zero) succeeded.");
665        // **************************************************************************************
666
667        // *** check refill volume of last hour (one) - *********************************************
668        let refill_volume_of_last_hour_one =
669            match sql_interface_refill.get_refill_volume_of_last_hour() {
670                Ok(c) => c,
671                Err(e) => panic!("Could not get refill volume from last hour (target=0.25): {e:?}"),
672            };
673        assert_eq!(refill_volume_of_last_hour_one, 0.25);
674        println!("* checking refill volume (target non-zero) succeeded.");
675        // **************************************************************************************
676
677        // *** check the content of last refill event - ********************************************
678        // Read back refill event entry from the database
679        let single_refill_event_entry_array = match sql_interface_refill.conn.query_map(
680            sql_query_strings::SQL_QUERY_READ_REFILL_EVENT,
681            |(timestamp, duration, volume, error_code)| SqlRefillEvent {
682                timestamp,
683                duration,
684                volume,
685                error_code,
686            },
687        ) {
688            Ok(c) => c,
689            Err(e) => {
690                panic!("Could not read refill event from data base: {e:?}");
691            }
692        };
693        // check if SQL response has the right structure
694        assert_eq!(single_refill_event_entry_array.len(), 1);
695        // check if SQL response contains the correct data
696        let refill_event = RefillEvent {
697            timestamp: NaiveDateTime::parse_from_str(
698                single_refill_event_entry_array[0].timestamp.as_str(),
699                "%Y-%m-%d %H:%M:%S",
700            )
701            .expect("conversion of timestamp to NaiveDate failed."),
702            duration: single_refill_event_entry_array[0].duration,
703            volume: single_refill_event_entry_array[0].volume,
704            error_code: single_refill_event_entry_array[0].error_code,
705        };
706        assert_eq!(
707            refill_event.timestamp,
708            naive_datetime_30min_ago.trunc_subsecs(0)
709        );
710        assert_eq!(refill_event.duration, 5.0);
711        assert_eq!(refill_event.volume, 0.25);
712        assert_eq!(refill_event.error_code, -2);
713
714        // *** Check max. number of rows detection ***
715        // refill table already has three rows at this point.
716        let test_result = SqlInterfaceRefill::new(sql_interface.get_connection().unwrap(), 1);
717        assert!(matches!(
718            test_result,
719            Err(SqlInterfaceError::DatabaseRefillTableContainsTooManyRows(
720                _,
721                _,
722                _
723            ))
724        ));
725        // **************************************************************************************
726    }
727}