testcontainers_modules/mariadb/
mod.rs

1use std::borrow::Cow;
2
3use testcontainers::{core::WaitFor, CopyDataSource, CopyToContainer, Image};
4
5const NAME: &str = "mariadb";
6const TAG: &str = "11.3";
7
8/// Module to work with [`MariaDB`] inside of tests.
9///
10/// Starts an instance of MariaDB with no password set for the root user and a default database named `test` created.
11///
12/// This module is based on the official [`MariaDB docker image`].
13///
14/// # Example
15/// ```
16/// use testcontainers_modules::{mariadb, testcontainers::runners::SyncRunner};
17///
18/// let mariadb_instance = mariadb::Mariadb::default().start().unwrap();
19/// let mariadb_url = format!(
20///     "mariadb://{}:{}/test",
21///     mariadb_instance.get_host().unwrap(),
22///     mariadb_instance.get_host_port_ipv4(3306).unwrap(),
23/// );
24/// ```
25///
26/// [`MariaDB`]: https://www.mariadb.com/
27/// [`MariaDB docker image`]: https://hub.docker.com/_/mariadb
28#[derive(Debug, Default, Clone)]
29pub struct Mariadb {
30    copy_to_sources: Vec<CopyToContainer>,
31}
32
33impl Mariadb {
34    /// Registers sql to be executed automatically when the container starts.
35    /// Can be called multiple times to add (not override) scripts.
36    ///
37    /// # Example
38    ///
39    /// ```
40    /// # use testcontainers_modules::mariadb::Mariadb;
41    /// let mariadb_image = Mariadb::default().with_init_sql(
42    ///     "CREATE TABLE foo (bar varchar(255));"
43    ///         .to_string()
44    ///         .into_bytes(),
45    /// );
46    /// ```
47    ///
48    /// ```rust,ignore
49    /// # use testcontainers_modules::mariadb::Mariadb;
50    /// let mariadb_image = Mariadb::default()
51    ///                                .with_init_sql(include_str!("path_to_init.sql").to_string().into_bytes());
52    /// ```
53    pub fn with_init_sql(mut self, init_sql: impl Into<CopyDataSource>) -> Self {
54        let target = format!(
55            "/docker-entrypoint-initdb.d/init_{i}.sql",
56            i = self.copy_to_sources.len()
57        );
58        self.copy_to_sources
59            .push(CopyToContainer::new(init_sql.into(), target));
60        self
61    }
62}
63
64impl Image for Mariadb {
65    fn name(&self) -> &str {
66        NAME
67    }
68
69    fn tag(&self) -> &str {
70        TAG
71    }
72
73    fn ready_conditions(&self) -> Vec<WaitFor> {
74        vec![
75            WaitFor::message_on_stderr("mariadbd: ready for connections."),
76            WaitFor::message_on_stderr("port: 3306"),
77        ]
78    }
79
80    fn env_vars(
81        &self,
82    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
83        [
84            ("MARIADB_DATABASE", "test"),
85            ("MARIADB_ALLOW_EMPTY_ROOT_PASSWORD", "1"),
86        ]
87    }
88    fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
89        &self.copy_to_sources
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use mysql::prelude::Queryable;
96    use testcontainers::core::IntoContainerPort;
97
98    use crate::{
99        mariadb::Mariadb as MariadbImage,
100        testcontainers::{runners::SyncRunner, ImageExt},
101    };
102
103    #[test]
104    fn mariadb_with_init_sql() -> Result<(), Box<dyn std::error::Error + 'static>> {
105        let node = MariadbImage::default()
106            .with_init_sql(
107                "CREATE TABLE foo (bar varchar(255));"
108                    .to_string()
109                    .into_bytes(),
110            )
111            .start()?;
112
113        let connection_string = &format!(
114            "mysql://root@{}:{}/test",
115            node.get_host()?,
116            node.get_host_port_ipv4(3306.tcp())?
117        );
118        let mut conn = mysql::Conn::new(mysql::Opts::from_url(connection_string).unwrap()).unwrap();
119
120        let rows: Vec<String> = conn.query("INSERT INTO foo(bar) VALUES ('blub')").unwrap();
121        assert_eq!(rows.len(), 0);
122
123        let rows: Vec<String> = conn.query("SELECT bar FROM foo").unwrap();
124        assert_eq!(rows.len(), 1);
125        Ok(())
126    }
127    #[test]
128    fn mariadb_one_plus_one() -> Result<(), Box<dyn std::error::Error + 'static>> {
129        let mariadb_image = MariadbImage::default();
130        let node = mariadb_image.start()?;
131
132        let connection_string = &format!(
133            "mysql://root@{}:{}/test",
134            node.get_host()?,
135            node.get_host_port_ipv4(3306.tcp())?
136        );
137        let mut conn = mysql::Conn::new(mysql::Opts::from_url(connection_string).unwrap()).unwrap();
138
139        let first_row = conn.query_first("SELECT 1 + 1;").unwrap();
140        assert_eq!(first_row, Some(2));
141
142        let first_column: i32 = first_row.unwrap();
143        assert_eq!(first_column, 2);
144        Ok(())
145    }
146
147    #[test]
148    fn mariadb_custom_version() -> Result<(), Box<dyn std::error::Error + 'static>> {
149        let image = MariadbImage::default().with_tag("11.2.3");
150        let node = image.start()?;
151
152        let connection_string = &format!(
153            "mysql://root@{}:{}/test",
154            node.get_host()?,
155            node.get_host_port_ipv4(3306.tcp())?
156        );
157
158        let mut conn = mysql::Conn::new(mysql::Opts::from_url(connection_string).unwrap()).unwrap();
159        let first_row: Option<String> = conn.query_first("SELECT version()").unwrap();
160        let first_column: String = first_row.unwrap();
161        assert!(
162            first_column.starts_with("11.2.3"),
163            "Expected version to start with 11.2.3, got: {first_column}",
164        );
165        Ok(())
166    }
167}