roadster/config/database/
mod.rs

1use crate::util::serde::default_true;
2use serde_derive::{Deserialize, Serialize};
3use serde_with::serde_as;
4use std::time::Duration;
5use url::Url;
6use validator::Validate;
7
8#[serde_as]
9#[derive(Debug, Clone, Validate, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11#[non_exhaustive]
12pub struct Database {
13    /// This can be overridden with an environment variable, e.g. `ROADSTER__DATABASE__URI=postgres://example:example@example:1234/example_app`
14    pub uri: Url,
15
16    /// Whether to automatically apply migrations during the app's start up. Migrations can also
17    /// be manually performed via the `roadster migration [COMMAND]` CLI command.
18    pub auto_migrate: bool,
19
20    #[serde(default = "Database::default_connect_timeout")]
21    #[serde_as(as = "serde_with::DurationMilliSeconds")]
22    pub connect_timeout: Duration,
23
24    /// Whether to attempt to connect to the DB immediately when the DB connection pool is created.
25    /// If `true` will wait to connect to the DB until the first DB query is attempted.
26    #[serde(default = "default_true")]
27    pub connect_lazy: bool,
28
29    #[serde(default = "Database::default_acquire_timeout")]
30    #[serde_as(as = "serde_with::DurationMilliSeconds")]
31    pub acquire_timeout: Duration,
32
33    #[serde_as(as = "Option<serde_with::DurationSeconds>")]
34    pub idle_timeout: Option<Duration>,
35
36    #[serde_as(as = "Option<serde_with::DurationSeconds>")]
37    pub max_lifetime: Option<Duration>,
38
39    #[serde(default)]
40    pub min_connections: u32,
41
42    pub max_connections: u32,
43
44    #[serde(default = "default_true")]
45    pub test_on_checkout: bool,
46
47    /// See [`bb8_8::Builder::retry_connection`]
48    #[cfg(feature = "db-diesel-pool-async")]
49    #[serde(default = "default_true")]
50    pub retry_connection: bool,
51
52    /// Create a temporary database in the same DB host from the `uri` field.
53    #[serde(default)]
54    pub temporary_test_db: bool,
55
56    /// Automatically clean up (drop) the temporary test DB that was created by setting
57    /// `temporary_test_db` to `true`. Note that the test DB will only be cleaned up if the closure
58    /// passed to [`crate::app::run_test`] or [`crate::app::run_test_with_result`] doesn't panic.
59    #[serde(default = "default_true")]
60    pub temporary_test_db_clean_up: bool,
61
62    /// Options for creating a Test Container instance for the DB. If enabled, the `Database#uri`
63    /// field will be overridden to be the URI for the Test Container instance that's created when
64    /// building the app's [`crate::app::context::AppContext`].
65    #[cfg(feature = "test-containers")]
66    #[serde(default)]
67    #[validate(nested)]
68    pub test_container: Option<crate::config::TestContainer>,
69}
70
71impl Database {
72    fn default_connect_timeout() -> Duration {
73        Duration::from_millis(1000)
74    }
75
76    fn default_acquire_timeout() -> Duration {
77        Duration::from_millis(1000)
78    }
79}
80
81#[cfg(feature = "db-sea-orm")]
82impl From<Database> for sea_orm::ConnectOptions {
83    fn from(database: Database) -> Self {
84        sea_orm::ConnectOptions::from(&database)
85    }
86}
87
88#[cfg(feature = "db-sea-orm")]
89impl From<&Database> for sea_orm::ConnectOptions {
90    fn from(database: &Database) -> Self {
91        let mut options = sea_orm::ConnectOptions::new(database.uri.to_string());
92        options
93            .test_before_acquire(database.test_on_checkout)
94            .connect_timeout(database.connect_timeout)
95            .connect_lazy(database.connect_lazy)
96            .acquire_timeout(database.acquire_timeout)
97            .min_connections(database.min_connections)
98            .max_connections(database.max_connections)
99            .sqlx_logging(false);
100        if let Some(idle_timeout) = database.idle_timeout {
101            options.idle_timeout(idle_timeout);
102        }
103        if let Some(max_lifetime) = database.max_lifetime {
104            options.max_lifetime(max_lifetime);
105        }
106        options
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::testing::snapshot::TestCase;
114    use rstest::fixture;
115
116    #[fixture]
117    #[cfg_attr(coverage_nightly, coverage(off))]
118    fn case() -> TestCase {
119        Default::default()
120    }
121
122    #[rstest::rstest]
123    #[case(
124        r#"
125        uri = "https://example.com:1234"
126        auto-migrate = true
127        max-connections = 1
128        "#
129    )]
130    #[case(
131        r#"
132        uri = "https://example.com:1234"
133        auto-migrate = true
134        max-connections = 1
135        connect-timeout = 1000
136        acquire-timeout = 2000
137        idle-timeout = 3000
138        max-lifetime = 4000
139        "#
140    )]
141    #[cfg(feature = "db-diesel-pool-async")]
142    #[cfg_attr(coverage_nightly, coverage(off))]
143    fn serialization(_case: TestCase, #[case] config: &str) {
144        let database: Database = toml::from_str(config).unwrap();
145
146        insta::assert_toml_snapshot!(database);
147    }
148
149    #[fixture]
150    #[cfg_attr(coverage_nightly, coverage(off))]
151    fn db_config() -> Database {
152        Database {
153            uri: Url::parse("postgres://example:example@example:1234/example_app").unwrap(),
154            #[cfg(feature = "test-containers")]
155            test_container: None,
156            auto_migrate: true,
157            connect_timeout: Duration::from_secs(1),
158            connect_lazy: true,
159            acquire_timeout: Duration::from_secs(2),
160            idle_timeout: Some(Duration::from_secs(3)),
161            max_lifetime: Some(Duration::from_secs(4)),
162            min_connections: 10,
163            max_connections: 20,
164            test_on_checkout: true,
165            #[cfg(feature = "db-diesel-pool-async")]
166            retry_connection: true,
167            temporary_test_db: false,
168            temporary_test_db_clean_up: false,
169        }
170    }
171
172    #[rstest::rstest]
173    #[cfg(feature = "db-sea-orm")]
174    #[cfg_attr(coverage_nightly, coverage(off))]
175    fn db_config_to_connect_options(db_config: Database) {
176        let connect_options = sea_orm::ConnectOptions::from(db_config);
177
178        insta::assert_debug_snapshot!(connect_options);
179    }
180
181    #[rstest::rstest]
182    #[cfg(feature = "db-sea-orm")]
183    #[cfg_attr(coverage_nightly, coverage(off))]
184    fn db_config_to_connect_options_ref(db_config: Database) {
185        let connect_options = sea_orm::ConnectOptions::from(&db_config);
186
187        insta::assert_debug_snapshot!(connect_options);
188    }
189}