Skip to main content

rightsize_modules/
mariadb.rs

1//! A single-node MariaDB container. Defaults to a `test`/`test`/`test`
2//! user/password/database trio (plus `MARIADB_ROOT_PASSWORD=test`) so
3//! [`MariaDbGuard::connection_string`] is usable with zero configuration; call
4//! [`MariaDbContainer::with_username`]/[`MariaDbContainer::with_password`]/
5//! [`MariaDbContainer::with_database`] before `start()` to override any of them.
6//!
7//! ### Readiness — empirically pinned, following [`crate::mysql::MySqlContainer`]'s precedent exactly
8//!
9//! The official `mariadb` entrypoint double-boots exactly like MySQL's: once as a
10//! throwaway "temp server" to run init scripts (which prints `ready for connections`
11//! with `port: 0`, i.e. no port bound yet), then for real on port 3306. Captured
12//! verbatim from a real `docker run mariadb:11.4` boot with this module's env
13//! (`MARIADB_USER=test`, `MARIADB_DATABASE=test`, `MARIADB_ROOT_PASSWORD=test`):
14//!
15//! ```text
16//! 2026-07-04  8:47:29 0 [Note] mariadbd: ready for connections.
17//! Version: '11.4.12-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 0  mariadb.org binary distribution
18//! 2026-07-04  8:47:30 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
19//! 2026-07-04  8:47:30 0 [Note] Server socket created on IP: '::', port: '3306'.
20//! 2026-07-04  8:47:30 0 [Note] mariadbd: ready for connections.
21//! Version: '11.4.12-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 3306  mariadb.org binary distribution
22//! ```
23//!
24//! Unlike MySQL 8.4, MariaDB has no X Plugin adding a third `ready for connections`
25//! line with a decoy `3306`-prefixed port (`33060`), so there's no false-match trap to
26//! anchor against — but the temp server's `port: 0` line still means an unanchored
27//! `times=2` count would be correct only by coincidence (it happens to work here
28//! because there are exactly two `ready for connections` lines total). This module
29//! follows the MySQL house precedent anyway and anchors on the literal `port: 3306`
30//! immediately followed, somewhere later on the same line, by `mariadb.org binary
31//! distribution` — so the wait is robust to the temp-server line's exact wording even
32//! if a future MariaDB point release changes it:
33//! `.*port: 3306.*mariadb\.org binary distribution.*`, via the ordinary
34//! [`Wait::for_log_message`] path.
35//!
36//! No `with_memory_limit` override needed — same InnoDB-footprint precedent as MySQL
37//! 8.4: boots clean on msb's default ~450M microVM RAM (observed ~14.8s IT round-trip
38//! on msb).
39
40use std::time::Duration;
41
42use rightsize::{Container, ContainerGuard, Result, Wait};
43
44const PORT: u16 = 3306;
45
46/// A single-node MariaDB container.
47pub struct MariaDbContainer {
48    container: Container,
49    username: String,
50    password: String,
51    database: String,
52}
53
54impl MariaDbContainer {
55    /// Builds a container from the pinned default image (`mariadb:11.4`).
56    pub fn new() -> Self {
57        Self::with_image("mariadb:11.4")
58    }
59
60    /// Builds a container from a caller-chosen image.
61    pub fn with_image(image: &str) -> Self {
62        let username = "test".to_string();
63        let password = "test".to_string();
64        let database = "test".to_string();
65        let container = Container::new(image)
66            .with_exposed_ports(&[PORT])
67            .with_env("MARIADB_USER", &username)
68            .with_env("MARIADB_PASSWORD", &password)
69            .with_env("MARIADB_DATABASE", &database)
70            .with_env("MARIADB_ROOT_PASSWORD", "test")
71            // Anchored on the real server's line (see the module doc for the
72            // captured log excerpt and why an unanchored "port: 3306" search would
73            // only be correct by coincidence here).
74            .waiting_for(
75                Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
76                    .with_startup_timeout(Duration::from_secs(60)),
77            );
78        Self {
79            container,
80            username,
81            password,
82            database,
83        }
84    }
85
86    /// Overrides `MARIADB_USER` (default `test`).
87    pub fn with_username(mut self, username: &str) -> Self {
88        self.username = username.to_string();
89        self.container = self.container.with_env("MARIADB_USER", username);
90        self
91    }
92
93    /// Overrides `MARIADB_PASSWORD` (default `test`).
94    pub fn with_password(mut self, password: &str) -> Self {
95        self.password = password.to_string();
96        self.container = self.container.with_env("MARIADB_PASSWORD", password);
97        self
98    }
99
100    /// Overrides `MARIADB_DATABASE` (default `test`).
101    pub fn with_database(mut self, database: &str) -> Self {
102        self.database = database.to_string();
103        self.container = self.container.with_env("MARIADB_DATABASE", database);
104        self
105    }
106
107    /// Boots the container.
108    pub async fn start(self) -> Result<MariaDbGuard> {
109        crate::register_default_backends();
110        let guard = self.container.start().await?;
111        Ok(MariaDbGuard {
112            guard,
113            username: self.username,
114            password: self.password,
115            database: self.database,
116        })
117    }
118}
119
120impl Default for MariaDbContainer {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126/// The running guard for a [`MariaDbContainer`].
127pub struct MariaDbGuard {
128    guard: ContainerGuard,
129    username: String,
130    password: String,
131    database: String,
132}
133
134impl MariaDbGuard {
135    /// The configured database user (default `test`).
136    pub fn username(&self) -> &str {
137        &self.username
138    }
139
140    /// The configured database password (default `test`).
141    pub fn password(&self) -> &str {
142        &self.password
143    }
144
145    /// The configured database name (default `test`).
146    pub fn database_name(&self) -> &str {
147        &self.database
148    }
149
150    /// A `mysql://` connection string for the running container's
151    /// [`Self::database_name`] — MariaDB speaks the MySQL wire protocol, so this
152    /// crate's house `mysql://` scheme (matching [`crate::mysql::MySqlGuard::connection_string`])
153    /// applies here too.
154    pub fn connection_string(&self) -> String {
155        format!(
156            "mysql://{}:{}@{}:{}/{}",
157            self.username,
158            self.password,
159            self.guard.host(),
160            self.guard.get_mapped_port(PORT).unwrap(),
161            self.database,
162        )
163    }
164
165    /// Stops and removes the container, releasing its host port.
166    pub async fn stop(self) -> Result<()> {
167        self.guard.stop().await
168    }
169}
170
171impl std::ops::Deref for MariaDbGuard {
172    type Target = ContainerGuard;
173    fn deref(&self) -> &ContainerGuard {
174        &self.guard
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use rightsize::wait::{WaitStrategy, WaitTarget};
182
183    #[test]
184    fn defaults_are_the_test_trio() {
185        let c = MariaDbContainer::new();
186        assert_eq!(c.username, "test");
187        assert_eq!(c.password, "test");
188        assert_eq!(c.database, "test");
189    }
190
191    #[test]
192    fn builders_override_the_defaults() {
193        let c = MariaDbContainer::new()
194            .with_username("alice")
195            .with_password("s3cret")
196            .with_database("app");
197        assert_eq!(c.username, "alice");
198        assert_eq!(c.password, "s3cret");
199        assert_eq!(c.database, "app");
200    }
201
202    const CAPTURED_LOG: &str = "\
2032026-07-04  8:47:29 0 [Note] mariadbd: ready for connections.
204Version: '11.4.12-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 0  mariadb.org binary distribution
2052026-07-04  8:47:30 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
2062026-07-04  8:47:30 0 [Note] Server socket created on IP: '::', port: '3306'.
2072026-07-04  8:47:30 0 [Note] mariadbd: ready for connections.
208Version: '11.4.12-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 3306  mariadb.org binary distribution";
209
210    struct FakeTarget(std::sync::Mutex<String>);
211    #[async_trait::async_trait]
212    impl WaitTarget for FakeTarget {
213        fn host(&self) -> &str {
214            "127.0.0.1"
215        }
216        fn mapped_port(&self, guest_port: u16) -> u16 {
217            guest_port
218        }
219        fn exposed_guest_ports(&self) -> Vec<u16> {
220            vec![PORT]
221        }
222        async fn current_logs(&self) -> String {
223            self.0.lock().unwrap().clone()
224        }
225        fn describe(&self) -> String {
226            "fake-mariadb".to_string()
227        }
228    }
229
230    // Pins the anchored pattern (`.*port: 3306.*mariadb\.org binary distribution.*`)
231    // against the temp server's `port: 0` line only — must not signal ready yet.
232    #[tokio::test]
233    async fn temp_server_port_zero_line_does_not_signal_ready() {
234        let partial: String = CAPTURED_LOG.lines().take(2).collect::<Vec<_>>().join("\n");
235        let target = FakeTarget(std::sync::Mutex::new(partial));
236        let err = Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
237            .with_startup_timeout(Duration::from_millis(300))
238            .wait_until_ready(&target)
239            .await
240            .expect_err("the temp server's port: 0 line must not signal ready");
241        let _ = err;
242    }
243
244    #[tokio::test]
245    async fn only_the_real_servers_port_3306_line_signals_ready() {
246        let target = FakeTarget(std::sync::Mutex::new(CAPTURED_LOG.to_string()));
247        Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
248            .with_startup_timeout(Duration::from_secs(5))
249            .wait_until_ready(&target)
250            .await
251            .expect("the real server's port: 3306 line must signal ready");
252    }
253}