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        let guard = self.container.start().await?;
110        Ok(MariaDbGuard {
111            guard,
112            username: self.username,
113            password: self.password,
114            database: self.database,
115        })
116    }
117}
118
119impl Default for MariaDbContainer {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// The running guard for a [`MariaDbContainer`].
126pub struct MariaDbGuard {
127    guard: ContainerGuard,
128    username: String,
129    password: String,
130    database: String,
131}
132
133impl MariaDbGuard {
134    /// The configured database user (default `test`).
135    pub fn username(&self) -> &str {
136        &self.username
137    }
138
139    /// The configured database password (default `test`).
140    pub fn password(&self) -> &str {
141        &self.password
142    }
143
144    /// The configured database name (default `test`).
145    pub fn database_name(&self) -> &str {
146        &self.database
147    }
148
149    /// A `mysql://` connection string for the running container's
150    /// [`Self::database_name`] — MariaDB speaks the MySQL wire protocol, so this
151    /// crate's house `mysql://` scheme (matching [`crate::mysql::MySqlGuard::connection_string`])
152    /// applies here too.
153    pub fn connection_string(&self) -> String {
154        format!(
155            "mysql://{}:{}@{}:{}/{}",
156            self.username,
157            self.password,
158            self.guard.host(),
159            self.guard.get_mapped_port(PORT).unwrap(),
160            self.database,
161        )
162    }
163
164    /// Stops and removes the container, releasing its host port.
165    pub async fn stop(self) -> Result<()> {
166        self.guard.stop().await
167    }
168}
169
170impl std::ops::Deref for MariaDbGuard {
171    type Target = ContainerGuard;
172    fn deref(&self) -> &ContainerGuard {
173        &self.guard
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use rightsize::wait::{WaitStrategy, WaitTarget};
181
182    #[test]
183    fn defaults_are_the_test_trio() {
184        let c = MariaDbContainer::new();
185        assert_eq!(c.username, "test");
186        assert_eq!(c.password, "test");
187        assert_eq!(c.database, "test");
188    }
189
190    #[test]
191    fn builders_override_the_defaults() {
192        let c = MariaDbContainer::new()
193            .with_username("alice")
194            .with_password("s3cret")
195            .with_database("app");
196        assert_eq!(c.username, "alice");
197        assert_eq!(c.password, "s3cret");
198        assert_eq!(c.database, "app");
199    }
200
201    const CAPTURED_LOG: &str = "\
2022026-07-04  8:47:29 0 [Note] mariadbd: ready for connections.
203Version: '11.4.12-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 0  mariadb.org binary distribution
2042026-07-04  8:47:30 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
2052026-07-04  8:47:30 0 [Note] Server socket created on IP: '::', port: '3306'.
2062026-07-04  8:47:30 0 [Note] mariadbd: ready for connections.
207Version: '11.4.12-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 3306  mariadb.org binary distribution";
208
209    struct FakeTarget(std::sync::Mutex<String>);
210    #[async_trait::async_trait]
211    impl WaitTarget for FakeTarget {
212        fn host(&self) -> &str {
213            "127.0.0.1"
214        }
215        fn mapped_port(&self, guest_port: u16) -> u16 {
216            guest_port
217        }
218        fn exposed_guest_ports(&self) -> Vec<u16> {
219            vec![PORT]
220        }
221        async fn current_logs(&self) -> String {
222            self.0.lock().unwrap().clone()
223        }
224        fn describe(&self) -> String {
225            "fake-mariadb".to_string()
226        }
227    }
228
229    // Pins the anchored pattern (`.*port: 3306.*mariadb\.org binary distribution.*`)
230    // against the temp server's `port: 0` line only — must not signal ready yet.
231    #[tokio::test]
232    async fn temp_server_port_zero_line_does_not_signal_ready() {
233        let partial: String = CAPTURED_LOG.lines().take(2).collect::<Vec<_>>().join("\n");
234        let target = FakeTarget(std::sync::Mutex::new(partial));
235        let err = Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
236            .with_startup_timeout(Duration::from_millis(300))
237            .wait_until_ready(&target)
238            .await
239            .expect_err("the temp server's port: 0 line must not signal ready");
240        let _ = err;
241    }
242
243    #[tokio::test]
244    async fn only_the_real_servers_port_3306_line_signals_ready() {
245        let target = FakeTarget(std::sync::Mutex::new(CAPTURED_LOG.to_string()));
246        Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
247            .with_startup_timeout(Duration::from_secs(5))
248            .wait_until_ready(&target)
249            .await
250            .expect("the real server's port: 3306 line must signal ready");
251    }
252}