aquarium_control/database/sql_interface.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//! Provides the core SQL database interface and connection management for the application.
11//!
12//! This module is the foundation of the application's database layer. It defines the
13//! main `SqlInterface` struct, which is responsible for establishing and managing a
14//! connection pool to the MySQL database. It also provides a suite of low-level
15//! helper functions for executing common query patterns, which are then used by
16//! more specialized `SqlInterface...` modules throughout the application.
17//!
18//! ## Key Components
19//!
20//! - **`SqlInterface` Struct**: The central struct that holds the `mysql::Pool`. It is
21//! instantiated once at application startup and provides a `get_connection()` method
22//! to lend out `PooledConn` objects to other threads and modules.
23//!
24//! - **`new()` Constructor**: Handles the critical task of initializing the database
25//! connection. It builds the connection URL, creates the pool, and performs
26//! essential pre-flight checks, such as verifying the database's `wait_timeout`
27//! setting to prevent unexpected connection drops.
28//!
29//! - **Generic Helper Functions**: A collection of private and public static methods like
30//! `get_single_string_from_database`, `get_single_integer_from_database`, and
31//! `get_timestamp`. These functions encapsulate common SQL query patterns (e.g.,
32//! fetching a single value, fetching an optional value) and provide consistent
33//! error handling.
34//!
35//! - **`ping_database()`**: A static method that executes a lightweight query (`SELECT 1`)
36//! to verify that a connection is still alive. This is the core implementation
37//! used by the `Pingable` trait to prevent connection timeouts.
38//!
39//! ## Design and Purpose
40//!
41//! The primary goals of this module are **efficiency, robustness, and centralization**.
42//!
43//! - **Connection Pooling**: By using `mysql::Pool`, the application avoids the high
44//! cost of opening and closing a new database connection for every operation. The pool
45//! manages a set of ready-to-use connections, which is essential for performance in a
46//! multithreaded environment.
47//!
48//! - **Startup Validation**: The `new()` constructor performs critical checks at startup,
49//! such as `check_wait_timeout` and `check_required_tables_existing`. This "fail-fast"
50//! approach ensures that the application does not start in a misconfigured or
51//! unstable state.
52//!
53//! - **Centralized Logic**: It centralizes the logic for creating connections and
54//! executing basic queries, preventing this boilerplate from being scattered across
55//! the application.
56
57use chrono::*;
58use mysql::prelude::*;
59use mysql::*;
60
61use crate::database::sql_interface_config::SqlInterfaceConfig;
62use crate::database::sql_query_strings::{SQL_QUERY_PING, SQL_QUERY_READ_TIMEOUT};
63use crate::database::{sql_interface_error::SqlInterfaceError, sql_query_strings};
64use crate::utilities::version_information::VersionInformation;
65
66/// Struct is used to retrieve a single integer value from the SQL database.
67struct SingleInteger {
68 single_integer: i64,
69}
70
71/// Struct is used to retrieve a single string value from the SQL database.
72struct SingleString {
73 single_string: String,
74}
75
76/// Struct is used to retrieve a single float value from the SQL database.
77struct SingleFloat {
78 single_float: f64,
79}
80
81/// Holds the configuration and the implementation of the SQL interface.
82pub struct SqlInterface {
83 /// Configuration data read from the .toml file
84 config: SqlInterfaceConfig,
85
86 /// Connection pool
87 pool: Pool,
88
89 /// Connection to the database (public because accessed from within test cases)
90 pub conn: PooledConn,
91}
92
93impl SqlInterface {
94 /// Creates a new `SqlInterface` instance and establishes a connection pool
95 /// to the SQL database based on the provided configuration.
96 ///
97 /// This function attempts to build a database connection URL from the
98 /// `SqlInterfaceConfig` and then uses it to create a connection pool.
99 /// It also retrieves an initial connection from the pool for the main thread.
100 ///
101 /// # Arguments
102 /// * `config` - Configuration data for the SQL interface, including
103 /// database user, password, host, port, and name.
104 ///
105 /// # Returns
106 /// A `Result` containing a new `SqlInterface` instance on success.
107 ///
108 /// # Errors
109 /// This function will return an error if:
110 /// - The connection pool cannot be established (`SqlInterfaceError::ConnectionPoolFailure`), for example,
111 /// due to an invalid URL, incorrect credentials, or network issues.
112 /// - An initial connection cannot be retrieved from the pool (`SqlInterfaceError::ConnectionFromPool`).
113 /// - The database's `wait_timeout` setting is too low (`SqlInterfaceError::WaitTimeoutTooLow`).
114 pub fn new(config: SqlInterfaceConfig) -> Result<SqlInterface, SqlInterfaceError> {
115 // Create the connection pool
116 let url = "mysql://".to_owned()
117 + &config.db_user
118 + ":"
119 + &config.db_password
120 + "@"
121 + &config.db_host
122 + ":"
123 + &config.db_port.to_string()
124 + "/"
125 + config.db_name.as_str();
126 let pool = match Pool::new(url.as_str()) {
127 Ok(c) => c,
128 Err(e) => {
129 return Err(SqlInterfaceError::ConnectionPoolFailure {
130 location: module_path!().to_string(),
131 url,
132 source: e,
133 });
134 }
135 };
136
137 // Get connection for requests of the main thread.
138 let mut conn = pool
139 .get_conn()
140 .map_err(|e| SqlInterfaceError::ConnectionFromPool {
141 location: module_path!().to_string(),
142 source: e,
143 })?;
144
145 // Call the refactored check_wait_timeout
146 Self::check_wait_timeout(&mut conn, config.db_min_wait_timeout)?;
147
148 Ok(SqlInterface { config, pool, conn })
149 }
150
151 /// Retrieves a new connection from the internal database connection pool.
152 ///
153 /// # Returns
154 /// A `Result` containing a `PooledConn` object on success.
155 ///
156 /// # Errors
157 /// This function will return an `Err(SqlInterfaceError::ConnectionFromPool)` if it
158 /// fails to retrieve a database connection from the pool. This typically indicates
159 /// a severe issue with database availability, pool exhaustion, or configuration.
160 pub fn get_connection(&self) -> Result<PooledConn, SqlInterfaceError> {
161 self.pool
162 .get_conn()
163 .map_err(|e| SqlInterfaceError::ConnectionFromPool {
164 location: module_path!().to_string(),
165 source: e,
166 })
167 }
168
169 /// Executes a given SQL query and maps the results into a vector of `SingleString` structs.
170 ///
171 /// # Arguments
172 /// * `conn` - A mutable reference to an active database connection.
173 /// * `sql_query_string` - The SQL query string to be executed.
174 ///
175 /// # Returns
176 /// A `Result` containing a vector of `SingleString` structs. The vector
177 /// may be empty if the query returns no rows.
178 ///
179 /// # Errors
180 /// This function will return an `(SqlInterfaceError::SingleStringRequestFailure)`
181 /// if the database query execution fails for any reason, such as a syntax error
182 /// in the SQL or a connection issue.
183 fn get_string_array_from_database(
184 conn: &mut PooledConn,
185 sql_query_string: &str,
186 ) -> Result<Vec<SingleString>, SqlInterfaceError> {
187 conn.query_map(sql_query_string, |single_string| SingleString {
188 single_string,
189 })
190 .map_err(|e| SqlInterfaceError::SingleStringRequestFailure {
191 location: module_path!().to_string(),
192 query: sql_query_string.to_string(),
193 source: e,
194 })
195 }
196
197 /// Executes an SQL query and strictly expects a single string result.
198 ///
199 /// # Arguments
200 /// * `conn` - A mutable reference to an active database connection.
201 /// * `sql_query_string` - The SQL query designed to return exactly one string value.
202 ///
203 /// # Returns
204 /// A `Result` containing the single `String` value on success.
205 ///
206 /// # Errors
207 /// This function will return an error if:
208 /// - The underlying database query fails.
209 /// - The query returns zero rows (`SqlInterfaceError::SingleStringRequestEmptyResponse`).
210 /// - The query returns more than one row (`SqlInterfaceError::SingleStringRequestNoSingleResponse`).
211 fn get_single_string_from_database(
212 conn: &mut PooledConn,
213 sql_query_string: &str,
214 ) -> Result<String, SqlInterfaceError> {
215 // Get a single string value from the database
216 let single_string_array = Self::get_string_array_from_database(conn, sql_query_string)?;
217
218 // check the error case if there is no response
219 if single_string_array.is_empty() {
220 return Err(SqlInterfaceError::SingleStringRequestEmptyResponse(
221 module_path!().to_string(),
222 sql_query_string.to_string(),
223 ));
224 }
225
226 // check the error case if there is more than one response
227 if single_string_array.len() > 1 {
228 return Err(SqlInterfaceError::SingleStringRequestNoSingleResponse(
229 module_path!().to_string(),
230 sql_query_string.to_string(),
231 ));
232 }
233 Ok(single_string_array[0].single_string.clone())
234 }
235
236 /// Retrieves the stored hash for a specific software version from the database.
237 ///
238 /// # Arguments
239 /// * `version_information` - A reference to a `VersionInformation` struct.
240 ///
241 /// # Returns
242 /// A `Result` containing the hash `String` associated with the specified version.
243 ///
244 /// # Errors
245 /// This function will return an error if:
246 /// - The database query fails (`SqlInterfaceError::SingleVersionRequestFailure`).
247 /// - No hash is found for the given version (`SqlInterfaceError::SingleVersionRequestNotListed`).
248 /// - More than one hash is found, indicating a data inconsistency (`SqlInterfaceError::SingleVersionRequestMultipleResults`).
249 pub fn get_hash_from_database(
250 &mut self,
251 version_information: &VersionInformation,
252 ) -> Result<String, SqlInterfaceError> {
253 // Get a single string value from the database
254 let params = params!(
255 "major" => version_information.major,
256 "minor" => version_information.minor,
257 "build" => version_information.build,
258 );
259
260 let single_string_array = self
261 .conn
262 .exec_map(
263 sql_query_strings::SQL_QUERY_VERSION_INFORMATION,
264 params,
265 |single_string| SingleString { single_string },
266 )
267 .map_err(|_| {
268 SqlInterfaceError::SingleVersionRequestFailure(
269 module_path!().to_string(),
270 sql_query_strings::SQL_QUERY_VERSION_INFORMATION.to_string(),
271 version_information.major,
272 version_information.minor,
273 version_information.build,
274 )
275 })?;
276
277 // check the error case if there is no response
278 if single_string_array.is_empty() {
279 return Err(SqlInterfaceError::SingleVersionRequestNotListed(
280 module_path!().to_string(),
281 sql_query_strings::SQL_QUERY_VERSION_INFORMATION.to_string(),
282 version_information.major,
283 version_information.minor,
284 version_information.build,
285 ));
286 }
287
288 // check the error case if there is more than one response
289 if single_string_array.len() > 1 {
290 Err(SqlInterfaceError::SingleVersionRequestMultipleResults(
291 module_path!().to_string(),
292 sql_query_strings::SQL_QUERY_VERSION_INFORMATION.to_string(),
293 version_information.major,
294 version_information.minor,
295 version_information.build,
296 ))
297 } else {
298 Ok(single_string_array[0].single_string.clone())
299 }
300 }
301
302 /// Executes an SQL query and collects multiple string results into a vector.
303 ///
304 /// This function is specifically designed for queries expected to return **more than one**
305 /// string value.
306 ///
307 /// # Arguments
308 /// * `sql_query_string` - The SQL query intended to retrieve multiple string values.
309 ///
310 /// # Returns
311 /// A `Result` containing a `Vec<String>` with all the string values retrieved from the database on success.
312 ///
313 /// # Errors
314 /// This function will return an error if:
315 /// - The underlying database query execution fails (`SqlInterfaceError::SingleStringRequestFailure`).
316 /// - The query returns zero rows (`SqlInterfaceError::MultipleStringRequestEmptyResponse`).
317 /// - The query returns only one row (`SqlInterfaceError::MultipleStringRequestSingleResponse`), as this function
318 /// strictly expects multiple results.
319 fn get_multiple_strings_from_database(
320 &mut self,
321 sql_query_string: &str,
322 ) -> Result<Vec<String>, SqlInterfaceError> {
323 // Get multiple string values from the database
324 let single_string_array =
325 match self
326 .conn
327 .query_map(sql_query_string, |single_string| SingleString {
328 single_string,
329 }) {
330 Ok(c) => c,
331 Err(e) => {
332 return Err(SqlInterfaceError::SingleStringRequestFailure {
333 location: module_path!().to_string(),
334 query: sql_query_string.to_string(),
335 source: e,
336 });
337 }
338 };
339
340 // check if there is no response or only one row
341 if single_string_array.is_empty() {
342 return Err(SqlInterfaceError::MultipleStringRequestEmptyResponse(
343 module_path!().to_string(),
344 sql_query_string.to_string(),
345 ));
346 } else if single_string_array.len() == 1 {
347 return Err(SqlInterfaceError::MultipleStringRequestSingleResponse(
348 module_path!().to_string(),
349 sql_query_string.to_string(),
350 ));
351 }
352
353 let mut response_array: Vec<String> = Vec::new();
354
355 for single_string_struct in single_string_array {
356 response_array.push(single_string_struct.single_string);
357 }
358 Ok(response_array)
359 }
360
361 /// Executes an SQL query and strictly expects a single integer result.
362 ///
363 /// This function is designed for queries that should return **exactly one**
364 /// integer value. It will return an error if the query yields no results
365 /// or more than one result.
366 ///
367 /// # Arguments
368 /// * `conn` - A mutable reference to an active database connection.
369 /// * `sql_query_string` - The SQL query designed to return precisely one integer.
370 ///
371 /// # Returns
372 /// A `Result` containing `i64`: The unique integer value retrieved from the database.
373 ///
374 /// # Errors
375 /// This function will return an error if:
376 /// - The database query returns zero rows or more than one row (`SqlInterfaceError::SingleIntegerRequestNoSingleResponse)`.
377 /// - The database query execution fails.`(SqlInterfaceError::SingleIntegerRequestFailure)`.
378 pub fn get_single_integer_from_database(
379 conn: &mut PooledConn,
380 sql_query_string: &str,
381 ) -> Result<i64, SqlInterfaceError> {
382 // Get a single integer value from the database
383 let single_integer_array = match conn.query_map(sql_query_string, |single_integer| {
384 SingleInteger { single_integer }
385 }) {
386 Ok(c) => c,
387 Err(_) => {
388 return Err(SqlInterfaceError::SingleIntegerRequestFailure(
389 module_path!().to_string(),
390 sql_query_string.to_string(),
391 ));
392 }
393 };
394
395 // check the error case if there is no response
396 if single_integer_array.is_empty() {
397 return Err(SqlInterfaceError::SingleIntegerRequestNoSingleResponse(
398 module_path!().to_string(),
399 sql_query_string.to_string(),
400 ));
401 }
402
403 // check the error case if there is more than one response
404 if single_integer_array.len() > 1 {
405 return Err(SqlInterfaceError::SingleIntegerRequestNoSingleResponse(
406 module_path!().to_string(),
407 sql_query_string.to_string(),
408 ));
409 }
410 Ok(single_integer_array[0].single_integer)
411 }
412
413 /// Executes an SQL query and strictly expects a single floating-point value.
414 ///
415 /// This function is tailored for queries that should return **exactly one**
416 /// floating-point number (`f64`). It will return an error if the query
417 /// results in zero rows or more than one row.
418 ///
419 /// # Arguments
420 /// * `conn` - A mutable reference to an active database connection.
421 /// * `sql_query_string` - The SQL query designed to return a single `f64` value.
422 ///
423 /// # Returns
424 /// A `Result` which contains `f64`: The unique floating-point value retrieved from the database.
425 ///
426 /// # Errors
427 /// This function will return an error if:
428 /// - The database query returns no rows or more than one row (`(SqlInterfaceError::SingleFloatRequestNoSingleResponse)`.
429 /// - The database query execution encounters an error `(SqlInterfaceError::SingleFloatRequestFailure`).
430 pub fn get_single_float_from_database(
431 conn: &mut PooledConn,
432 sql_query_string: &str,
433 ) -> Result<f64, SqlInterfaceError> {
434 // Get a single float value from the database
435 let single_float_array = match conn.query_map(sql_query_string, |single_float| {
436 SingleFloat { single_float }
437 }) {
438 Ok(c) => c,
439 Err(_) => {
440 return Err(SqlInterfaceError::SingleFloatRequestFailure(
441 module_path!().to_string(),
442 sql_query_string.to_string(),
443 ));
444 }
445 };
446
447 // check the error case if there is no response
448 if single_float_array.is_empty() {
449 return Err(SqlInterfaceError::SingleFloatRequestEmptyResponse(
450 module_path!().to_string(),
451 sql_query_string.to_string(),
452 ));
453 }
454
455 // check the error case if there is more than one response
456 if single_float_array.len() > 1 {
457 return Err(SqlInterfaceError::SingleFloatRequestNoSingleResponse(
458 module_path!().to_string(),
459 sql_query_string.to_string(),
460 ));
461 }
462 Ok(single_float_array[0].single_float)
463 }
464
465 /// Retrieves the name of the currently connected SQL database.
466 ///
467 /// This function executes an internal SQL query to fetch the database name.
468 ///
469 /// # Returns
470 /// A `Result` which `String`: The name of the database as a string.
471 ///
472 /// # Errors
473 /// This function will return an error if:
474 /// - there is any issue retrieving the database name, such as a communication
475 /// error or an unexpected response from the database `(SqlInterfaceError::DatabaseNameRequestFailure)`.
476 pub fn get_database(&mut self) -> Result<String, SqlInterfaceError> {
477 // trigger SQL call
478 Self::get_single_string_from_database(&mut self.conn, sql_query_strings::SQL_QUERY_DATABASE)
479 .map_err(|e| SqlInterfaceError::DatabaseNameRequestFailure {
480 location: module_path!().to_string(),
481 source: Box::new(e),
482 })
483 }
484
485 /// Executes an SQL query to retrieve a single timestamp string and converts it to `NaiveDateTime`.
486 ///
487 /// This function fetches a single timestamp from the database of type `NaiveDateTime`.
488 ///
489 /// # Arguments
490 /// * `conn` - A mutable reference to an active database connection.
491 /// * `sql_query_string` - The SQL query to retrieve the timestamp as a string.
492 ///
493 /// # Returns
494 /// A `Result` containing `NaiveDateTime` on success.
495 ///
496 /// # Errors
497 /// This function will return an error if:
498 /// - The underlying database query to fetch the string fails (`SqlInterfaceError::TimestampRequestFailure`).
499 /// This can happen if the query is invalid, the connection is lost, or if the query
500 /// does not return at least one row.
501 /// - The retrieved string cannot be parsed into a valid `NaiveDateTime`
502 /// (`SqlInterfaceError::TimestampConversionFailure`). This indicates the database
503 /// returned a string in an unexpected format.
504 pub fn get_timestamp(
505 conn: &mut PooledConn,
506 sql_query_string: &str,
507 ) -> Result<NaiveDateTime, SqlInterfaceError> {
508 // request timestamp from database
509 let timestamp_opt: Option<NaiveDateTime> =
510 conn.query_first(sql_query_string).map_err(|_| {
511 SqlInterfaceError::TimestampRequestFailure {
512 location: module_path!().to_string(),
513 query: sql_query_string.to_string(),
514 }
515 })?;
516
517 // Check if we got a result and extract the value.
518 match timestamp_opt {
519 Some(timestamp) => Ok(timestamp),
520 None => Err(SqlInterfaceError::TimestampRequestFailure {
521 location: module_path!().to_string(),
522 query: sql_query_string.to_string(),
523 }),
524 }
525 }
526
527 /// Executes an SQL query to retrieve an optional timestamp and converts it to `NaiveDateTime`.
528 ///
529 /// This function is designed for queries that might return a single timestamp, but where an
530 /// empty result is also a valid outcome (e.g., checking for the last event in a log that might be empty).
531 /// It fetches a string (if present, expected in "YYYY-MM-DD HH:MM:SS" format)
532 /// and attempts to parse it.
533 ///
534 /// # Arguments
535 /// * `conn` - A mutable reference to an active database connection.
536 /// * `sql_query_string` - The SQL query to retrieve the optional timestamp as a string.
537 ///
538 /// # Returns
539 /// A `Result` containing an `Option<NaiveDateTime>` on success:
540 /// - `Ok(Some(NaiveDateTime))`: If the query returns exactly one row with a parsable timestamp string.
541 /// - `Ok(None)`: If the query returns no rows.
542 ///
543 /// # Errors
544 /// This function will return an error if:
545 /// - The underlying database query fails for reasons like a syntax error or connection issue.
546 /// The original error is wrapped in `SqlInterfaceError::OptionalTimestampRequestFailure`.
547 /// - The query returns more than one row, which is considered a data inconsistency. This is also
548 /// wrapped in `SqlInterfaceError::OptionalTimestampRequestFailure`.
549 /// - The retrieved string cannot be parsed into a valid `NaiveDateTime`, which returns
550 /// `SqlInterfaceError::TimestampConversionFailure`. This indicates the database
551 /// returned a string in an unexpected format.
552 pub fn get_optional_timestamp(
553 conn: &mut PooledConn,
554 sql_query_string: &str,
555 ) -> Result<Option<NaiveDateTime>, SqlInterfaceError> {
556 // Request (optional) timestamp from the database.
557 conn.query_first(sql_query_string).map_err(|_| {
558 SqlInterfaceError::OptionalTimestampRequestFailure {
559 location: module_path!().to_string(),
560 query: sql_query_string.to_string(),
561 }
562 })
563 }
564
565 /// Fetches the current timestamp directly from the database.
566 ///
567 /// This function executes a database query to get the database server's current timestamp
568 /// and returns it as a `NaiveDateTime`.
569 ///
570 /// # Arguments
571 /// * `conn` - A mutable reference to an active database connection.
572 ///
573 /// # Returns
574 /// A `Result` containing the current `NaiveDateTime` from the database on success.
575 ///
576 /// # Errors
577 /// This function will return an `Err` variant of `SqlInterfaceError` if:
578 /// - The underlying database query to fetch the timestamp string fails. This can be due to
579 /// a connection issue, an invalid query, or if the query does not return exactly one row.
580 /// The original error is wrapped in `SqlInterfaceError::TimestampRequestFailure`.
581 /// - The timestamp string returned by the database cannot be parsed into a `NaiveDateTime`.
582 /// This indicates a data format mismatch and returns `SqlInterfaceError::TimestampConversionFailure`.
583 pub fn get_current_timestamp(
584 conn: &mut PooledConn,
585 ) -> Result<NaiveDateTime, SqlInterfaceError> {
586 Self::get_timestamp(conn, sql_query_strings::SQL_QUERY_READ_CURRENT_TIMESTAMP)
587 }
588
589 #[cfg(test)]
590 /// Retrieves a timestamp from the database with a specified future or past offset.
591 ///
592 /// This function is exclusively for use in test environments. It queries the database
593 /// for its current timestamp and then adds a given number of seconds to it, effectively
594 /// simulating a future or past point in time for testing scenarios.
595 ///
596 /// # Arguments
597 /// * `conn` - A mutable reference to an active database connection.
598 /// * `offset_seconds` - The number of seconds to add to the current database timestamp.
599 /// A positive value shifts the timestamp into the future, while a negative value
600 /// shifts it into the past.
601 ///
602 /// # Returns
603 /// A `Result` containing the calculated `NaiveDateTime` (offset by the given seconds) on success.
604 ///
605 /// # Errors
606 /// This function will return an `Err` variant of `SqlInterfaceError` if:
607 /// - The underlying database query to fetch the timestamp string fails. This can be due to
608 /// a connection issue, an invalid query, or if the query does not return exactly one row.
609 /// The original error is wrapped in `SqlInterfaceError::TimestampRequestFailure`.
610 /// - The timestamp string returned by the database cannot be parsed into a `NaiveDateTime`.
611 /// This indicates a data format mismatch and returns `SqlInterfaceError::TimestampConversionFailure`.
612 pub fn get_current_timestamp_offset_seconds(
613 conn: &mut PooledConn,
614 offset_seconds: i32,
615 ) -> Result<NaiveDateTime, SqlInterfaceError> {
616 let offset_seconds_string = offset_seconds.to_string();
617 let sql_query_with_offset_resolved = str::replace(
618 sql_query_strings::SQL_QUERY_CURRENT_TIMESTAMP_OFFSET_SECONDS,
619 "#offset",
620 &offset_seconds_string,
621 );
622 Self::get_timestamp(conn, &sql_query_with_offset_resolved)
623 }
624
625 /// Executes an SQL query to retrieve a single date of type `NaiveDate`.
626 ///
627 /// This function fetches a date from the database.
628 ///
629 /// # Arguments
630 /// * `conn` - A mutable reference to an active database connection.
631 /// * `sql_query_string` - The SQL query designed to retrieve the date as a string.
632 ///
633 /// # Returns
634 /// A `Result` containing the successfully parsed `NaiveDate` on success.
635 ///
636 /// # Errors
637 /// This function will return an error if:
638 /// - The underlying database query to fetch the string fails (`SqlInterfaceError::DateRequestFailure`).
639 /// This can happen if the query is invalid, the connection is lost, or if the query
640 /// does not return exactly one row.
641 fn get_date(
642 conn: &mut PooledConn,
643 sql_query_string: &str,
644 ) -> Result<NaiveDate, SqlInterfaceError> {
645 let date_opt: Option<(NaiveDate,)> = conn.query_first(sql_query_string).map_err(|_| {
646 SqlInterfaceError::DateRequestFailure {
647 location: module_path!().to_string(),
648 query: sql_query_string.to_string(),
649 }
650 })?;
651
652 // Handle the case where the query returned no rows.
653 date_opt
654 .map(|(date,)| date) // Extracts the NaiveDate from the tuple `(NaiveDate)`
655 .ok_or_else(|| {
656 // Create the specific error for an empty response, just like the old code did.
657 SqlInterfaceError::DateRequestFailure {
658 location: module_path!().to_string(),
659 query: sql_query_string.to_string(),
660 }
661 })
662 }
663
664 /// Fetches the current calendar date directly from the database.
665 ///
666 /// This function executes a standard SQL query to get the database server's current date
667 /// and returns it as a `NaiveDate`.
668 ///
669 /// # Arguments
670 /// * `conn` - A mutable reference to an active database connection.
671 ///
672 /// # Returns
673 /// A `Result` containing the current `NaiveDate` from the database on success.
674 ///
675 /// # Errors
676 /// This function will return an `Err` variant of `SqlInterfaceError` if:
677 /// - The underlying database query to fetch the date string fails. This can be due to
678 /// a connection issue, an invalid query, or if the query does not return exactly one row.
679 /// The original error is wrapped in `SqlInterfaceError::DateRequestFailure`.
680 /// - The date string returned by the database cannot be parsed into a `NaiveDate`.
681 /// This indicates a data format mismatch and returns `SqlInterfaceError::TimestampConversionFailure`.
682 pub fn get_current_date(conn: &mut PooledConn) -> Result<NaiveDate, SqlInterfaceError> {
683 Self::get_date(conn, sql_query_strings::SQL_QUERY_READ_CURRENT_DATE)
684 }
685
686 /// Fetches the calendar date for the following day directly from the database.
687 ///
688 /// This function executes a standard SQL query to get the database server's date for "tomorrow"
689 /// and returns it as a `NaiveDate`.
690 ///
691 /// # Arguments
692 /// * `conn` - A mutable reference to an active database connection.
693 ///
694 /// # Returns
695 /// A `Result` containing tomorrow's `NaiveDate` from the database on success.
696 ///
697 /// # Errors
698 /// This function will return an `Err` variant of `SqlInterfaceError` if:
699 /// - The underlying database query to fetch the date string fails. This can be due to
700 /// a connection issue, an invalid query, or if the query does not return exactly one row.
701 /// The original error is wrapped in `SqlInterfaceError::DateRequestFailure`.
702 /// - The date string returned by the database cannot be parsed into a `NaiveDate`.
703 /// This indicates a data format mismatch and returns `SqlInterfaceError::TimestampConversionFailure`.
704 pub fn get_tomorrow_date(conn: &mut PooledConn) -> Result<NaiveDate, SqlInterfaceError> {
705 Self::get_date(conn, sql_query_strings::SQL_QUERY_READ_TOMORROW_DATE)
706 }
707
708 /// Fetches the names of all tables present in the connected SQL database.
709 ///
710 /// This function executes a standard SQL query to list all tables.
711 ///
712 /// # Returns
713 /// A `Result` containing a `Vec<String>` with the names of all tables found in the database.
714 /// The vector may be empty if the database contains no tables.
715 ///
716 /// # Errors
717 /// This function will return an `Err` if the underlying database query to list tables
718 /// fails. The specific error will likely be `SqlInterfaceError::SingleStringRequestFailure`,
719 /// which can be caused by a connection issue, lack of permissions, or other
720 /// database-side errors.
721 pub fn get_tables(&mut self) -> Result<Vec<String>, SqlInterfaceError> {
722 match self.get_multiple_strings_from_database(sql_query_strings::SQL_QUERY_READ_TABLES) {
723 Ok(c) => Ok(c),
724 Err(_) => Err(SqlInterfaceError::ShowTablesWithoutProperResult(
725 module_path!().to_string(),
726 )),
727 }
728 }
729
730 /// Determines if all required table names are present within a list of existing tables.
731 ///
732 /// This private helper function checks if every string in `required_tables`
733 /// can be found within `existing_tables`. The comparison is case-sensitive.
734 ///
735 /// # Arguments
736 /// * `required_tables` - A `Vec<String>` containing the names of tables that
737 /// are expected to exist.
738 /// * `existing_tables` - A `Vec<String>` containing the names of tables
739 /// actually found in the database.
740 ///
741 /// # Returns
742 /// `true` if every table name in `required_tables` is found in `existing_tables`;
743 /// `false` otherwise.
744 fn compare_required_tables_existing_tables(
745 required_tables: Vec<String>,
746 existing_tables: Vec<String>,
747 ) -> bool {
748 for required_table in required_tables {
749 if !existing_tables.contains(&required_table) {
750 return false;
751 }
752 }
753 true
754 }
755
756 /// Verifies that all tables specified in the configuration exist in the database.
757 ///
758 /// This function first retrieves a list of all tables currently in the database.
759 /// It then compares this list against the `db_tables` defined in the
760 /// `SqlInterfaceConfig` to ensure all required tables are present.
761 ///
762 /// # Returns
763 /// A `Result` which is `Ok(())` on success, indicating that all tables listed in the
764 /// configuration were found in the database.
765 ///
766 /// # Errors
767 /// This function will return an error if:
768 /// - One or more required tables are missing from the database. This will result in
769 /// an `SqlInterfaceError::RequiredTablesNotExisting` error, which includes the
770 /// list of required tables and the list of tables that were actually found.
771 /// - The underlying call to `get_tables` fails. This will propagate the error,
772 /// which could be due to a database connection issue or insufficient permissions.
773 pub fn check_required_tables_existing(&mut self) -> Result<(), SqlInterfaceError> {
774 let existing_tables = self.get_tables()?;
775
776 if !Self::compare_required_tables_existing_tables(
777 self.config.db_tables.clone(),
778 existing_tables.clone(),
779 ) {
780 let required_tables_string = self.config.db_tables.join(",");
781 let existing_tables_string = existing_tables.join(",");
782 Err(SqlInterfaceError::RequiredTablesNotExisting(
783 module_path!().to_string(),
784 required_tables_string,
785 existing_tables_string,
786 ))
787 } else {
788 Ok(())
789 }
790 }
791
792 /// Checks if the database's `wait_timeout` setting meets a minimum required value.
793 ///
794 /// This function retrieves the current `wait_timeout` from the connected database
795 /// and compares it against a `min_wait_timeout` provided by the application.
796 /// It is primarily used to ensure that database connections do not time out
797 /// prematurely from the server's perspective, which could lead to "Broken pipe" errors.
798 /// The function is called in the initialization phase of the control application.
799 ///
800 /// # Arguments
801 ///
802 /// * `conn` - A mutable reference to a `PooledConn` (a database connection from a pool).
803 /// This connection is used to execute the SQL query to fetch the `wait_timeout`.
804 /// * `min_wait_timeout` - The minimum acceptable `wait_timeout` value (in seconds)
805 /// that the application requires for stable operation.
806 ///
807 /// # Errors
808 ///
809 /// This function will return an error in the following scenarios:
810 /// - If it fails to retrieve the `wait_timeout` value from the database (e.g., due to
811 /// connection issues, query execution errors).
812 /// - If the retrieved `wait_timeout` value from the database is not a valid unsigned 64-bit integer
813 /// (e.g., if the database returns a value that cannot be converted to `u64`). This indicates
814 /// an unexpected data type or format from the database.
815 /// - If the `current_wait_timeout` read from the database is less than `min_wait_timeout`.
816 /// In this case, the application will return an error with a message indicating the discrepancy,
817 /// as it considers an insufficient `wait_timeout` a critical configuration error.
818 pub fn check_wait_timeout(
819 conn: &mut PooledConn,
820 min_wait_timeout: u64,
821 ) -> Result<(), SqlInterfaceError> {
822 let current_wait_timeout_i64 =
823 Self::get_single_integer_from_database(conn, SQL_QUERY_READ_TIMEOUT)?;
824
825 let current_wait_timeout: u64 = current_wait_timeout_i64.try_into().map_err(|_| {
826 SqlInterfaceError::WaitTimeoutInvalid(
827 module_path!().to_string(),
828 current_wait_timeout_i64,
829 )
830 })?;
831
832 if current_wait_timeout < min_wait_timeout && min_wait_timeout != 0 {
833 return Err(SqlInterfaceError::WaitTimeoutTooLow(
834 module_path!().to_string(),
835 current_wait_timeout,
836 min_wait_timeout,
837 ));
838 }
839 Ok(())
840 }
841
842 /// Pings the database to check connection health and prevent timeouts.
843 ///
844 /// This function executes a lightweight SQL query (`SQL_QUERY_PING`, typically `SELECT 1;`)
845 /// against the provided database connection. Its primary purpose is to:
846 /// 1. **Keep the connection alive:** By sending a query, it resets the database server's
847 /// idle timeout counters (`wait_timeout`), preventing the
848 /// server from unilaterally closing the connection due to inactivity.
849 /// 2. **Verify connection status: ** A successful ping indicates that the connection
850 /// is still active and able to communicate with the database.
851 ///
852 /// If the ping query fails (e.g., due to a broken connection, network issue, or
853 /// database server unavailability), an error is logged, and an `Err` is returned.
854 ///
855 /// # Arguments
856 ///
857 /// * `conn` - A mutable reference to a `PooledConn`, representing an active
858 /// database connection obtained from a connection pool.
859 ///
860 /// # Returns
861 /// An empty `Result` (`Ok(())`) on success, indicating the connection is healthy.
862 ///
863 /// # Errors
864 /// This function will return an `Err(SqlInterfaceError::DatabasePingFailure)` if the
865 /// ping query fails. This typically indicates that the connection is no longer valid,
866 /// which could be caused by:
867 /// - A network interruption between the application and the database server.
868 /// - The database server being shut down or restarted.
869 /// - The connection being explicitly closed by the server due to exceeding the
870 /// `wait_timeout` or other resource limits.
871 pub fn ping_database(conn: &mut PooledConn) -> Result<(), SqlInterfaceError> {
872 SqlInterface::get_single_integer_from_database(conn, SQL_QUERY_PING)
873 .map_err(|e| SqlInterfaceError::DatabasePingFailure {
874 location: module_path!().to_string(),
875 source: Box::new(e),
876 })
877 .map(|_| ()) // On success, transform the i64 into a unit type
878 }
879
880 #[cfg(test)]
881 // This helper function **empties the specified database table** using a TRUNCATE command.
882 // The function is only used in test environments across different crates to ensure a clean state
883 // before running tests.
884 //
885 // Arguments:
886 // * `sql_interface`: A mutable reference to the main SQL interface.
887 // * `sql_table_name`: The name of the table to be emptied.
888 //
889 /// # Returns
890 /// An empty `Result` (`Ok(())`) on successful truncation of the table.
891 ///
892 /// # Errors
893 /// This function will return an `Err(SqlInterfaceError::TruncateTableFailure)` if the
894 /// `TRUNCATE TABLE` command fails. This can happen for several reasons, including
895 /// - The specified table does not exist.
896 /// - The database user lacks the necessary permissions to truncate the table.
897 /// - The database connection has been lost.
898 pub fn truncate_table(
899 sql_interface: &mut SqlInterface,
900 sql_table_name: String,
901 ) -> Result<(), SqlInterfaceError> {
902 let sql_query_string = str::replace(
903 sql_query_strings::SQL_QUERY_TRUNCATE_TABLE,
904 "#table",
905 &sql_table_name,
906 );
907 match sql_interface.conn.query_drop(sql_query_string) {
908 Ok(_) => Ok(()),
909 Err(e) => {
910 println!("{e:?}");
911 Err(SqlInterfaceError::TruncateTableFailure(
912 module_path!().to_string(),
913 sql_table_name,
914 ))
915 }
916 }
917 }
918
919 #[cfg(test)]
920 // This test-only function calculates a `NaiveDateTime` that is a specific duration
921 // in the past relative to the current local time. This function is used for setting up
922 // historical data scenarios in tests across different crates.
923 //
924 // Arguments:
925 // * `hours_in_past`: The number of hours back in time.
926 // * `seconds_in_past`: The number of additional seconds back in time.
927 //
928 // Returns:
929 // A `NaiveDateTime` representing the calculated past timestamp.
930 pub fn get_naive_timestamp_from_past(
931 hours_in_past: i64,
932 seconds_in_past: i64,
933 ) -> NaiveDateTime {
934 let timestamp_from_past =
935 Local::now() - Duration::hours(hours_in_past) - Duration::seconds(seconds_in_past);
936 timestamp_from_past.naive_local()
937 }
938
939 #[cfg(test)]
940 // This test-only function calculates the time difference in seconds between a
941 // provided `NaiveDateTime` (expected to be in the past) and the current local time.
942 //
943 // Arguments:
944 // * `timestamp_naive`: The past timestamp from which the duration will be measured.
945 //
946 // Returns:
947 // `Some(Duration)` representing the duration in seconds if the conversion and calculation
948 // are successful. Returns `None` if the `timestamp_naive` cannot be unambiguously
949 // converted to a local `DateTime`.
950 pub fn get_duration_from_naive_timestamp_to_local_now(
951 timestamp_naive: NaiveDateTime,
952 ) -> Option<Duration> {
953 use chrono::LocalResult::{Ambiguous, None, Single};
954
955 let reference_current_timestamp = Local::now();
956 let timestamp_dt: DateTime<Local> = match Local.from_local_datetime(×tamp_naive) {
957 Single(t) => t,
958 Ambiguous(_t1, _t2) => {
959 return Option::None;
960 }
961 None => {
962 return Option::None;
963 }
964 };
965 Some(reference_current_timestamp.signed_duration_since(timestamp_dt))
966 }
967}
968
969#[cfg(test)]
970pub mod tests {
971 use chrono::{DateTime, Local, NaiveDate, SubsecRound, TimeZone};
972 use mysql::params;
973 use mysql::prelude::Queryable;
974
975 use crate::database::sql_interface::{SqlInterface, SqlInterfaceError};
976 use crate::utilities::config::{read_config_file_with_test_database, ConfigData};
977 use crate::utilities::version_information::VersionInformation;
978
979 #[test]
980 // This test case checks the functionality of get_string_array_from_database.
981 // - response when querying an empty table (should be Ok with an empty Vec)
982 // - response when querying a table with one entry (should be Ok with a Vec of 1)
983 // - response when querying a table with multiple entries (should be Ok with a Vec of n)
984 // Test case uses test database #56.
985 fn test_get_string_array_from_database() {
986 // Setup
987 let config: ConfigData = read_config_file_with_test_database(
988 "/config/aquarium_control_test_generic.toml".to_string(),
989 56,
990 );
991 println!("Testing with database {}", config.sql_interface.db_name);
992 let sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
993 .expect("Initialization of SQL interface for test failed.");
994 let mut conn = sql_interface.get_connection().unwrap();
995
996 let table_name = "test_string_array";
997
998 // Create a temporary table for the test
999 conn.query_drop(&format!(
1000 "CREATE TABLE IF NOT EXISTS {} (name VARCHAR(255));",
1001 table_name
1002 ))
1003 .unwrap();
1004
1005 // --- Test Case 1: Empty table ---
1006 println!("* Testing string array with empty table...");
1007 conn.query_drop(&format!("TRUNCATE TABLE {}", table_name))
1008 .unwrap();
1009
1010 let result = SqlInterface::get_string_array_from_database(
1011 &mut conn,
1012 &format!("SELECT name FROM {}", table_name),
1013 );
1014 let result_vec = result.unwrap();
1015 assert!(
1016 result_vec.is_empty(),
1017 "Expected an empty vector for an empty table"
1018 );
1019 println!("* Succeeded: Empty table returns an empty Vec.");
1020
1021 // --- Test Case 2: Table with one entry ---
1022 println!("* Testing string array with a single entry...");
1023 let test_string_1 = "Hello";
1024 conn.exec_drop(
1025 &format!("INSERT INTO {} (name) VALUES (:name)", table_name),
1026 params! { "name" => test_string_1 },
1027 )
1028 .unwrap();
1029
1030 let result = SqlInterface::get_string_array_from_database(
1031 &mut conn,
1032 &format!("SELECT name FROM {}", table_name),
1033 );
1034 let result_vec = result.unwrap();
1035 assert_eq!(result_vec.len(), 1);
1036 assert_eq!(result_vec[0].single_string, test_string_1);
1037 println!("* Succeeded: Table with one entry returns a Vec with one element.");
1038
1039 // --- Test Case 3: Table with multiple entries ---
1040 println!("* Testing string array with multiple entries...");
1041 let test_string_2 = "World";
1042 conn.exec_drop(
1043 &format!("INSERT INTO {} (name) VALUES (:name)", table_name),
1044 params! { "name" => test_string_2 },
1045 )
1046 .unwrap();
1047
1048 // Use ORDER BY to ensure a predictable order for assertions
1049 let result = SqlInterface::get_string_array_from_database(
1050 &mut conn,
1051 &format!("SELECT name FROM {} ORDER BY name ASC", table_name),
1052 );
1053 let result_vec = result.unwrap();
1054 assert_eq!(result_vec.len(), 2);
1055 assert_eq!(result_vec[0].single_string, test_string_1); // "Hello"
1056 assert_eq!(result_vec[1].single_string, test_string_2); // "World"
1057 println!("* Succeeded: Table with multiple entries returns a Vec with all elements.");
1058
1059 // Cleanup
1060 conn.query_drop(&format!("DROP TABLE {}", table_name))
1061 .unwrap();
1062 }
1063
1064 #[test]
1065 // This test case checks the functionality of get_optional_timestamp.
1066 // - response when querying an empty table (should be Ok(None))
1067 // - response when querying a table with one entry (should be Ok(Some(timestamp)))
1068 // - error response when querying a table with multiple entries
1069 // Test case uses test database #56.
1070 fn test_get_optional_timestamp() {
1071 // Setup
1072 let config: ConfigData = read_config_file_with_test_database(
1073 "/config/aquarium_control_test_generic.toml".to_string(),
1074 56,
1075 );
1076 println!("Testing with database {}", config.sql_interface.db_name);
1077 let sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1078 .expect("Initialization of SQL interface for test failed.");
1079 let mut conn = sql_interface.get_connection().unwrap();
1080
1081 let table_name = "test_optional_timestamp";
1082
1083 // Create a temporary table for the test
1084 conn.query_drop(&format!(
1085 "CREATE TABLE IF NOT EXISTS {} (ts TIMESTAMP);",
1086 table_name
1087 ))
1088 .unwrap();
1089
1090 // --- Test Case 1: Empty table ---
1091 println!("* Testing optional timestamp with empty table...");
1092 conn.query_drop(&format!("TRUNCATE TABLE {}", table_name))
1093 .unwrap();
1094
1095 let result = SqlInterface::get_optional_timestamp(
1096 &mut conn,
1097 &format!("SELECT ts FROM {}", table_name),
1098 );
1099 assert_eq!(result.unwrap(), None);
1100 println!("* Succeeded: Empty table returns Ok(None).");
1101
1102 // --- Test Case 2: Table with one entry ---
1103 println!("* Testing optional timestamp with a single entry...");
1104 let test_timestamp = Local::now().naive_local().trunc_subsecs(0);
1105 conn.exec_drop(
1106 &format!("INSERT INTO {} (ts) VALUES (:ts)", table_name),
1107 params! { "ts" => test_timestamp },
1108 )
1109 .unwrap();
1110
1111 let result = SqlInterface::get_optional_timestamp(
1112 &mut conn,
1113 &format!("SELECT ts FROM {}", table_name),
1114 );
1115 assert_eq!(result.unwrap(), Some(test_timestamp));
1116 println!("* Succeeded: Table with one entry returns Ok(Some(timestamp)).");
1117
1118 // --- Test Case 3: Table with multiple entries ---
1119 println!("* Testing optional timestamp with multiple entries...");
1120 let another_timestamp = (Local::now() - chrono::Duration::minutes(5))
1121 .naive_local()
1122 .trunc_subsecs(0);
1123 conn.exec_drop(
1124 &format!("INSERT INTO {} (ts) VALUES (:ts)", table_name),
1125 params! { "ts" => another_timestamp },
1126 )
1127 .unwrap();
1128
1129 let result = SqlInterface::get_optional_timestamp(
1130 &mut conn,
1131 &format!("SELECT ts FROM {}", table_name),
1132 );
1133 assert_eq!(result.unwrap(), Some(test_timestamp));
1134 println!("* Succeeded: Table with multiple entries returns Ok(Some(timestamp)).");
1135
1136 // Cleanup
1137 conn.query_drop(&format!("DROP TABLE {}", table_name))
1138 .unwrap();
1139 }
1140
1141 #[test]
1142 // This test case checks if current timestamp is local timestamp.
1143 // It does not modify any database.
1144 pub fn test_get_current_timestamp() {
1145 let config: ConfigData = read_config_file_with_test_database(
1146 "/config/aquarium_control_test_generic.toml".to_string(),
1147 28,
1148 );
1149 println!("Testing with database {}", config.sql_interface.db_name);
1150 let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1151 .expect("Initialization of SQL interface for test failed.");
1152
1153 // *** check if current timestamp is local timestamp ***************************************
1154 let sql_timestamp = match SqlInterface::get_current_timestamp(&mut sql_interface.conn) {
1155 Ok(c) => c,
1156 Err(_) => panic!("Could not get current timestamp from the database."),
1157 };
1158 let date_time: DateTime<Local> = Local.from_local_datetime(&sql_timestamp).unwrap();
1159 assert_eq!(date_time, Local::now().trunc_subsecs(0));
1160 println!("* checking current timestamp succeeded.");
1161 // *****************************************************************************************
1162 }
1163
1164 #[test]
1165 // This test case checks the functionality of get_current_timestamp_offset_seconds.
1166 // It verifies that the function correctly returns timestamps in the future (positive offset),
1167 // in the past (negative offset), and at the present (zero offset).
1168 // Test case does not modify any database.
1169 fn test_get_current_timestamp_offset_seconds() {
1170 // Setup
1171 let config: ConfigData = read_config_file_with_test_database(
1172 "/config/aquarium_control_test_generic.toml".to_string(),
1173 56,
1174 );
1175 println!("Testing with database {}", config.sql_interface.db_name);
1176 let sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1177 .expect("Initialization of SQL interface for test failed.");
1178 let mut conn = sql_interface.get_connection().unwrap();
1179
1180 // --- Test Case 1: Positive offset (future) ---
1181 println!("* Testing timestamp with positive offset...");
1182 let offset_future: i64 = 3600; // 1 hour in the future
1183 let result_future =
1184 SqlInterface::get_current_timestamp_offset_seconds(&mut conn, offset_future as i32)
1185 .unwrap();
1186 let expected_future = Local::now().naive_local() + chrono::Duration::seconds(offset_future);
1187 let diff_future = (expected_future - result_future).num_seconds().abs();
1188 assert!(
1189 diff_future <= 2,
1190 "Future offset test failed. Expected ~{}, got {}. Difference: {}s",
1191 expected_future,
1192 result_future,
1193 diff_future
1194 );
1195 println!("* Succeeded: Positive offset returns a future timestamp.");
1196
1197 // --- Test Case 2: Negative offset (past) ---
1198 println!("* Testing timestamp with negative offset...");
1199 let offset_past: i64 = -1800; // 30 minutes in the past
1200 let result_past =
1201 SqlInterface::get_current_timestamp_offset_seconds(&mut conn, offset_past as i32)
1202 .unwrap();
1203 let expected_past = Local::now().naive_local() + chrono::Duration::seconds(offset_past);
1204 let diff_past = (expected_past - result_past).num_seconds().abs();
1205 assert!(
1206 diff_past <= 2,
1207 "Past offset test failed. Expected ~{}, got {}. Difference: {}s",
1208 expected_past,
1209 result_past,
1210 diff_past
1211 );
1212 println!("* Succeeded: Negative offset returns a past timestamp.");
1213
1214 // --- Test Case 3: Zero offset ---
1215 println!("* Testing timestamp with zero offset...");
1216 let offset_zero: i64 = 0;
1217 let result_zero =
1218 SqlInterface::get_current_timestamp_offset_seconds(&mut conn, offset_zero as i32)
1219 .unwrap();
1220 let expected_now = Local::now().naive_local();
1221 let diff_now = (expected_now - result_zero).num_seconds().abs();
1222 assert!(
1223 diff_now <= 2,
1224 "Zero offset test failed. Expected ~{}, got {}. Difference: {}s",
1225 expected_now,
1226 result_zero,
1227 diff_now
1228 );
1229 println!("* Succeeded: Zero offset returns the current timestamp.");
1230 }
1231
1232 #[test]
1233 // This test case checks the functionality of the private `get_date` function
1234 // and its public wrappers.
1235 // - Verifies the error response when a query returns no rows.
1236 // - Verifies the correct response when a query returns exactly one row.
1237 // - Verifies that `get_current_date` and `get_tomorrow_date` work as expected.
1238 // Test case uses test database #56.
1239 fn test_get_date() {
1240 // Setup
1241 let config: ConfigData = read_config_file_with_test_database(
1242 "/config/aquarium_control_test_generic.toml".to_string(),
1243 56,
1244 );
1245 println!("Testing with database {}", config.sql_interface.db_name);
1246 let sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1247 .expect("Initialization of SQL interface for test failed.");
1248 let mut conn = sql_interface.get_connection().unwrap();
1249
1250 let table_name = "test_date_table";
1251
1252 // Create a temporary table for the test
1253 conn.query_drop(&format!(
1254 "CREATE TABLE IF NOT EXISTS {} (d DATE);",
1255 table_name
1256 ))
1257 .unwrap();
1258
1259 // --- Test Case 1: Empty table (should error) ---
1260 println!("* Testing get_date with empty table...");
1261 conn.query_drop(&format!("TRUNCATE TABLE {}", table_name))
1262 .unwrap();
1263
1264 let result = SqlInterface::get_date(&mut conn, &format!("SELECT d FROM {}", table_name));
1265
1266 // Assert that we get the specific error for an empty response.
1267 if let Err(SqlInterfaceError::DateRequestFailure { .. }) = result {
1268 // This is the expected error path.
1269 } else {
1270 panic!(
1271 "Expected DateRequestFailure for empty result, but got: {:?}",
1272 result
1273 );
1274 }
1275 println!("* Succeeded: Empty table returns a DateRequestFailure error.");
1276
1277 // --- Test Case 2: Table with one entry ---
1278 println!("* Testing get_date with a single entry...");
1279 let test_date = NaiveDate::from_ymd_opt(2025, 7, 15).unwrap();
1280 conn.exec_drop(
1281 &format!("INSERT INTO {} (d) VALUES (:d)", table_name),
1282 params! { "d" => test_date },
1283 )
1284 .unwrap();
1285
1286 let result = SqlInterface::get_date(&mut conn, &format!("SELECT d FROM {}", table_name));
1287 assert_eq!(result.unwrap(), test_date);
1288 println!("* Succeeded: Table with one entry returns Ok(date).");
1289
1290 // --- Test Case 3: Test public wrappers ---
1291 println!("* Testing get_current_date and get_tomorrow_date wrappers...");
1292 let current_date_from_db = SqlInterface::get_current_date(&mut conn).unwrap();
1293 let tomorrow_date_from_db = SqlInterface::get_tomorrow_date(&mut conn).unwrap();
1294
1295 let local_today = Local::now().date_naive();
1296 let local_tomorrow = local_today.succ_opt().unwrap();
1297
1298 assert_eq!(current_date_from_db, local_today);
1299 assert_eq!(tomorrow_date_from_db, local_tomorrow);
1300 println!("* Succeeded: Public wrappers return correct dates.");
1301
1302 // Cleanup
1303 conn.query_drop(&format!("DROP TABLE {}", table_name))
1304 .unwrap();
1305 }
1306
1307 #[test]
1308 // This test case checks
1309 // - if required tables are existing
1310 // Test case uses test database #28.
1311 pub fn test_sql_interface_required_tables_existing() {
1312 let config: ConfigData = read_config_file_with_test_database(
1313 "/config/aquarium_control_test_generic.toml".to_string(),
1314 28,
1315 );
1316 println!("Testing with database {}", config.sql_interface.db_name);
1317 let database_name_from_config = config.sql_interface.db_name.clone();
1318 let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1319 .expect("Initialization of SQL interface for test failed.");
1320
1321 let database_name_from_db = match sql_interface.get_database() {
1322 Ok(c) => c,
1323 Err(e) => panic!("Could not get database name: {e:?}"),
1324 };
1325 assert_eq!(database_name_from_db, database_name_from_config);
1326 println!(
1327 "* checking database name succeeded. database_name_from_db={}",
1328 database_name_from_db
1329 );
1330
1331 // *** check if required tables are existing - *********************************************
1332 let result_check_required_tables_existing = sql_interface.check_required_tables_existing();
1333 if let Err(e) = result_check_required_tables_existing {
1334 panic!("* checking required tables failed: {e:?}")
1335 } else {
1336 println!("* checking required tables succeeded.");
1337 }
1338 // *****************************************************************************************
1339
1340 // *** check if required tables are existing - *********************************************
1341 let dummy_table = "dummy".to_string();
1342 sql_interface.config.db_tables.push(dummy_table.clone());
1343 assert!(matches!(
1344 sql_interface.check_required_tables_existing(),
1345 Err(SqlInterfaceError::RequiredTablesNotExisting(_, _, _))
1346 ));
1347 println!("* checking required tables succeeded.");
1348 // *****************************************************************************************
1349
1350 // *** check if the current date is local date *********************************************
1351 let sql_date =
1352 match SqlInterface::get_current_date(&mut sql_interface.get_connection().unwrap()) {
1353 Ok(c) => c,
1354 Err(_) => panic!("Could not get current date from the database."),
1355 };
1356 let sql_date_string = sql_date.format("%Y-%m-%d").to_string();
1357 let local_date_string = Local::now().trunc_subsecs(0).format("%Y-%m-%d").to_string();
1358 assert_eq!(sql_date_string, local_date_string);
1359 println!("* checking current date succeeded.");
1360 // *****************************************************************************************
1361 }
1362
1363 #[test]
1364 // This test case verifies that `check_wait_timeout` successfully validates
1365 // that the database's `wait_timeout` setting is greater than or equal to
1366 // the minimum value specified in the configuration file.
1367 // Test case does not modify any database.
1368 fn test_check_wait_timeout_success() {
1369 // Setup
1370 let config: ConfigData = read_config_file_with_test_database(
1371 "/config/aquarium_control_test_generic.toml".to_string(),
1372 56,
1373 );
1374 println!("Testing with database {}", config.sql_interface.db_name);
1375 let min_wait_timeout_from_config = config.sql_interface.db_min_wait_timeout;
1376
1377 // We need a connection to run the check. The `new` function itself
1378 // already runs this check, but we call it explicitly for a focused test.
1379 let sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1380 .expect("Initialization of SQL interface for test failed.");
1381 let mut conn = sql_interface.get_connection().unwrap();
1382
1383 // --- Test Execution ---
1384 println!(
1385 "* Testing check_wait_timeout with value from config: {}s",
1386 min_wait_timeout_from_config
1387 );
1388 let result = SqlInterface::check_wait_timeout(&mut conn, min_wait_timeout_from_config);
1389
1390 // --- Assertion ---
1391 assert!(
1392 result.is_ok(),
1393 "check_wait_timeout failed, but was expected to succeed. Result: {:?}",
1394 result
1395 );
1396 println!("* Succeeded: Database wait_timeout is sufficient.");
1397 }
1398
1399 #[test]
1400 // This test case checks if a zero value for the minimum wait timeout allows instantiation.
1401 pub fn test_sql_interface_min_wait_timeout_check_deactivated() {
1402 let mut config: ConfigData = read_config_file_with_test_database(
1403 "/config/aquarium_control_test_generic.toml".to_string(),
1404 28,
1405 );
1406 config.sql_interface.db_min_wait_timeout = 0;
1407 println!("Testing with database {}", config.sql_interface.db_name);
1408 _ = SqlInterface::new(config.sql_interface)
1409 .expect("Initialization of SQL interface for test failed with min wait timeout=0.");
1410 }
1411
1412 #[test]
1413 // This test case checks if an excessively high value for the minimum wait timeout triggers an error.
1414 pub fn test_sql_interface_min_wait_timeout_check() {
1415 let mut config: ConfigData = read_config_file_with_test_database(
1416 "/config/aquarium_control_test_generic.toml".to_string(),
1417 28,
1418 );
1419 config.sql_interface.db_min_wait_timeout = 1000000;
1420 println!("Testing with database {}", config.sql_interface.db_name);
1421 let result = SqlInterface::new(config.sql_interface);
1422
1423 assert!(matches!(
1424 result,
1425 Err(SqlInterfaceError::WaitTimeoutTooLow(_, _, _))
1426 ));
1427 }
1428
1429 pub const SQL_QUERY_WRITE_VERSION_INFORMATION: &str =
1430 "INSERT into version(major, minor, build, hash) values(:major, :minor, :build, :hash);";
1431
1432 #[test]
1433 // This test case checks
1434 // - error response when querying empty table
1435 // - error response when version information does not match entry in database
1436 // - check error response when the hash is incorrect
1437 // Test case uses test database #29.
1438 pub fn test_sql_interface_version() {
1439 let config: ConfigData = read_config_file_with_test_database(
1440 "/config/aquarium_control_test_generic.toml".to_string(),
1441 29,
1442 );
1443 println!("Testing with database {}", config.sql_interface.db_name);
1444 let mut sql_interface: SqlInterface = SqlInterface::new(config.sql_interface)
1445 .expect("Initialization of SQL interface for test failed.");
1446
1447 // *** check error response when querying empty table **************************************
1448 let version_information = VersionInformation::new().unwrap();
1449 match SqlInterface::truncate_table(&mut sql_interface, "version".to_string()) {
1450 Ok(_) => {}
1451 Err(e) => panic!("Could not prepare test case: {e:?}"),
1452 }
1453 match sql_interface.get_hash_from_database(&version_information) {
1454 Ok(_) => {
1455 panic!("Found version information in truncated table.");
1456 }
1457 Err(e) => {
1458 assert!(matches!(
1459 e,
1460 SqlInterfaceError::SingleVersionRequestNotListed(_, _, _, _, _)
1461 ));
1462 }
1463 }
1464 // *****************************************************************************************
1465
1466 // *** check error response when version information does not match entry in database ******
1467 let unknown_version_information = VersionInformation {
1468 major: -1,
1469 minor: -1,
1470 build: -1,
1471 hash: "void".to_string(),
1472 };
1473 let sql_query_string = SQL_QUERY_WRITE_VERSION_INFORMATION.to_string();
1474 match sql_interface.conn.exec_drop::<_, _>(
1475 sql_query_string,
1476 params! {
1477 "major" => unknown_version_information.major,
1478 "minor" => unknown_version_information.minor,
1479 "build" => unknown_version_information.build,
1480 "hash" => unknown_version_information.hash.clone(),
1481 },
1482 ) {
1483 Ok(_) => {}
1484 Err(e) => {
1485 panic!("Error when inserting version information into database: {e:?}");
1486 }
1487 };
1488 match sql_interface.get_hash_from_database(&version_information) {
1489 Ok(_) => {
1490 panic!("Found correct version information although having inserted incorrect version information table.");
1491 }
1492 Err(e) => {
1493 assert!(matches!(
1494 e,
1495 SqlInterfaceError::SingleVersionRequestNotListed(_, _, _, _, _)
1496 ))
1497 }
1498 }
1499
1500 // *** check response when the hash is correct ********************************************
1501 let version_information_with_wrong_hash = VersionInformation {
1502 major: version_information.major,
1503 minor: version_information.minor,
1504 build: version_information.build,
1505 hash: "wrong".to_string(),
1506 };
1507 let sql_query_string = SQL_QUERY_WRITE_VERSION_INFORMATION.to_string();
1508 match sql_interface.conn.exec_drop::<_, _>(
1509 sql_query_string,
1510 params! {
1511 "major" => version_information_with_wrong_hash.major,
1512 "minor" => version_information_with_wrong_hash.minor,
1513 "build" => version_information_with_wrong_hash.build,
1514 "hash" => version_information_with_wrong_hash.hash.clone(),
1515 },
1516 ) {
1517 Ok(_) => {}
1518 Err(e) => {
1519 panic!("Error when inserting version information into database: {e:?}");
1520 }
1521 };
1522 match sql_interface.get_hash_from_database(&version_information) {
1523 Ok(c) => {
1524 let reference_hash = &version_information_with_wrong_hash.hash;
1525 assert_eq!(&c, reference_hash);
1526 }
1527 Err(e) => {
1528 panic!("Error retrieving version information from the database: {e:?}");
1529 }
1530 }
1531 // ***************************************************************************************
1532 }
1533
1534 #[test]
1535 // This test case checks if the ping query can be executed
1536 pub fn test_sql_interface_ping() {
1537 let config: ConfigData = read_config_file_with_test_database(
1538 "/config/aquarium_control_test_generic.toml".to_string(),
1539 28,
1540 );
1541 let sql_interface = SqlInterface::new(config.sql_interface)
1542 .expect("Initialization of SQL interface for test failed");
1543
1544 SqlInterface::ping_database(&mut sql_interface.get_connection().unwrap()).unwrap();
1545 }
1546}