Skip to main content

pg_embedded_setup_unpriv/cluster/
lifecycle.rs

1//! Database lifecycle operations for `TestClusterConnection`.
2//!
3//! This module provides methods for creating, dropping, and managing databases
4//! on a running `PostgreSQL` cluster.
5
6use std::sync::{Mutex, OnceLock};
7
8use color_eyre::eyre::WrapErr;
9use dashmap::DashMap;
10use tracing::info_span;
11
12use super::connection::{TestClusterConnection, escape_identifier};
13use super::temporary_database::TemporaryDatabase;
14use crate::error::BootstrapResult;
15
16/// A strongly-typed database name for use with lifecycle operations.
17///
18/// This newtype provides type safety for database name parameters, preventing
19/// accidental misuse of raw strings while still allowing convenient conversion
20/// from string literals.
21///
22/// # Examples
23///
24/// ```
25/// use pg_embedded_setup_unpriv::DatabaseName;
26///
27/// // From string literal
28/// let name: DatabaseName = "my_database".into();
29/// assert_eq!(name.as_str(), "my_database");
30///
31/// // From owned String
32/// let name: DatabaseName = String::from("another_db").into();
33/// assert_eq!(name.as_str(), "another_db");
34/// ```
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct DatabaseName(String);
37
38impl DatabaseName {
39    /// Creates a new `DatabaseName` from a string.
40    #[must_use]
41    pub fn new(name: impl Into<String>) -> Self {
42        Self(name.into())
43    }
44
45    /// Returns the database name as a string slice.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl AsRef<str> for DatabaseName {
53    fn as_ref(&self) -> &str {
54        &self.0
55    }
56}
57
58impl From<&str> for DatabaseName {
59    fn from(s: &str) -> Self {
60        Self(s.to_owned())
61    }
62}
63
64impl From<String> for DatabaseName {
65    fn from(s: String) -> Self {
66        Self(s)
67    }
68}
69
70/// Global per-template locks to prevent concurrent template creation.
71///
72/// Uses a `DashMap` to allow lock-free reads and concurrent access to
73/// different templates while serialising access to the same template.
74static TEMPLATE_LOCKS: OnceLock<DashMap<String, Mutex<()>>> = OnceLock::new();
75
76fn template_locks() -> &'static DashMap<String, Mutex<()>> {
77    TEMPLATE_LOCKS.get_or_init(DashMap::new)
78}
79
80impl TestClusterConnection {
81    /// Executes a DDL command for database creation or deletion.
82    ///
83    /// This private helper consolidates the common pattern of escaping an
84    /// identifier, formatting SQL, and executing it via `batch_execute`.
85    fn execute_ddl_command(
86        &self,
87        sql_template: &str,
88        name: &str,
89        error_msg_verb: &str,
90    ) -> BootstrapResult<()> {
91        let mut client = self.admin_client()?;
92        let escaped = escape_identifier(name);
93        let sql = sql_template.replace("{}", &format!("\"{escaped}\""));
94        client
95            .batch_execute(&sql)
96            .wrap_err(format!("failed to {error_msg_verb} database '{name}'"))
97            .map_err(crate::error::BootstrapError::from)
98    }
99
100    /// Creates a new database with the given name.
101    ///
102    /// Connects to the `postgres` database as superuser and executes
103    /// `CREATE DATABASE`.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the database already exists or if the connection
108    /// fails.
109    ///
110    /// # Examples
111    ///
112    /// ```no_run
113    /// use pg_embedded_setup_unpriv::TestCluster;
114    ///
115    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
116    /// let cluster = TestCluster::new()?;
117    /// cluster.connection().create_database("my_test_db")?;
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub fn create_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
122        let db_name = name.into();
123        let _span = info_span!("create_database", db = %db_name.as_str()).entered();
124        self.execute_ddl_command("CREATE DATABASE {}", db_name.as_str(), "create")
125    }
126
127    /// Creates a new database by cloning an existing template.
128    ///
129    /// Connects to the `postgres` database as superuser and executes
130    /// `CREATE DATABASE ... TEMPLATE`. This is significantly faster than
131    /// creating an empty database and running migrations, as `PostgreSQL`
132    /// performs a filesystem-level copy.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if:
137    /// - The target database already exists
138    /// - The template database does not exist
139    /// - The template database has active connections
140    /// - The connection fails
141    ///
142    /// # Examples
143    ///
144    /// ```no_run
145    /// use pg_embedded_setup_unpriv::TestCluster;
146    ///
147    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
148    /// let cluster = TestCluster::new()?;
149    ///
150    /// // Create and set up a template database
151    /// cluster.connection().create_database("my_template")?;
152    /// // ... run migrations on my_template ...
153    ///
154    /// // Clone the template for a test
155    /// cluster.connection().create_database_from_template("test_db", "my_template")?;
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub fn create_database_from_template(
160        &self,
161        name: impl Into<DatabaseName>,
162        template: impl Into<DatabaseName>,
163    ) -> BootstrapResult<()> {
164        let db_name = name.into();
165        let template_name = template.into();
166        let _span =
167            info_span!("create_database_from_template", db = %db_name.as_str(), template = %template_name.as_str()).entered();
168        let mut client = self.admin_client()?;
169        let escaped_name = escape_identifier(db_name.as_str());
170        let escaped_template = escape_identifier(template_name.as_str());
171        let sql = format!("CREATE DATABASE \"{escaped_name}\" TEMPLATE \"{escaped_template}\"");
172        client
173            .batch_execute(&sql)
174            .wrap_err(format!(
175                "failed to create database '{}' from template '{}'",
176                db_name.as_str(),
177                template_name.as_str()
178            ))
179            .map_err(crate::error::BootstrapError::from)
180    }
181
182    /// Drops an existing database.
183    ///
184    /// Connects to the `postgres` database as superuser and executes
185    /// `DROP DATABASE`.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the database does not exist, has active connections,
190    /// or if the connection fails.
191    ///
192    /// # Examples
193    ///
194    /// ```no_run
195    /// use pg_embedded_setup_unpriv::TestCluster;
196    ///
197    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
198    /// let cluster = TestCluster::new()?;
199    /// cluster.connection().create_database("temp_db")?;
200    /// cluster.connection().drop_database("temp_db")?;
201    /// # Ok(())
202    /// # }
203    /// ```
204    pub fn drop_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
205        let db_name = name.into();
206        let _span = info_span!("drop_database", db = %db_name.as_str()).entered();
207        self.execute_ddl_command("DROP DATABASE {}", db_name.as_str(), "drop")
208    }
209
210    /// Checks whether a database with the given name exists.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if the connection fails.
215    ///
216    /// # Examples
217    ///
218    /// ```no_run
219    /// use pg_embedded_setup_unpriv::TestCluster;
220    ///
221    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
222    /// let cluster = TestCluster::new()?;
223    /// assert!(cluster.connection().database_exists("postgres")?);
224    /// assert!(!cluster.connection().database_exists("nonexistent")?);
225    /// # Ok(())
226    /// # }
227    /// ```
228    pub fn database_exists(&self, name: impl Into<DatabaseName>) -> BootstrapResult<bool> {
229        let db_name = name.into();
230        let mut client = self.admin_client()?;
231        let row = client
232            .query_one(
233                "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)",
234                &[&db_name.as_str()],
235            )
236            .wrap_err("failed to query pg_database")
237            .map_err(crate::error::BootstrapError::from)?;
238        Ok(row.get(0))
239    }
240
241    /// Ensures a template database exists, creating it if necessary.
242    ///
243    /// Uses per-template locking to prevent concurrent creation attempts when
244    /// multiple tests race to initialise the same template. The `setup_fn` is
245    /// called only if the template does not already exist.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if database creation fails or if `setup_fn` returns
250    /// an error.
251    ///
252    /// # Examples
253    ///
254    /// ```no_run
255    /// use pg_embedded_setup_unpriv::TestCluster;
256    ///
257    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
258    /// let cluster = TestCluster::new()?;
259    ///
260    /// // Ensure template exists, running migrations if needed
261    /// cluster.connection().ensure_template_exists("my_template", |db_name| {
262    ///     // Run migrations on the newly created template database
263    ///     // e.g., diesel::migration::run(&mut conn)?;
264    ///     Ok(())
265    /// })?;
266    ///
267    /// // Clone the template for each test
268    /// cluster.connection().create_database_from_template("test_db_1", "my_template")?;
269    /// # Ok(())
270    /// # }
271    /// ```
272    pub fn ensure_template_exists<F>(
273        &self,
274        name: impl Into<DatabaseName>,
275        setup_fn: F,
276    ) -> BootstrapResult<()>
277    where
278        F: FnOnce(&str) -> BootstrapResult<()>,
279    {
280        let db_name = name.into();
281        let _span = info_span!("ensure_template_exists", template = %db_name.as_str()).entered();
282        let locks = template_locks();
283        let lock = locks
284            .entry(db_name.as_str().to_owned())
285            .or_insert_with(|| Mutex::new(()));
286        let _guard = lock
287            .lock()
288            .unwrap_or_else(std::sync::PoisonError::into_inner);
289
290        if !self.database_exists(db_name.as_str())? {
291            self.create_database(db_name.as_str())?;
292            setup_fn(db_name.as_str())?;
293        }
294        Ok(())
295    }
296
297    /// Creates a temporary database that is dropped when the guard is dropped.
298    ///
299    /// This is useful for test isolation where each test creates its own
300    /// database and the database is automatically cleaned up when the test
301    /// completes.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the database already exists or if the connection
306    /// fails.
307    ///
308    /// # Examples
309    ///
310    /// ```no_run
311    /// use pg_embedded_setup_unpriv::TestCluster;
312    ///
313    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
314    /// let cluster = TestCluster::new()?;
315    /// let temp_db = cluster.connection().temporary_database("my_temp_db")?;
316    ///
317    /// // Database is dropped automatically when temp_db goes out of scope
318    /// let url = temp_db.url();
319    /// # Ok(())
320    /// # }
321    /// ```
322    pub fn temporary_database(
323        &self,
324        name: impl Into<DatabaseName>,
325    ) -> BootstrapResult<TemporaryDatabase> {
326        let db_name = name.into();
327        let _span = info_span!("temporary_database", db = %db_name.as_str()).entered();
328        self.create_database(db_name.as_str())?;
329        Ok(TemporaryDatabase::new(
330            db_name.as_str().to_owned(),
331            self.database_url("postgres"),
332            self.database_url(db_name.as_str()),
333        ))
334    }
335
336    /// Creates a temporary database from a template.
337    ///
338    /// Combines template cloning with RAII cleanup. The database is created
339    /// by cloning the template and is automatically dropped when the guard
340    /// goes out of scope.
341    ///
342    /// # Errors
343    ///
344    /// Returns an error if the target database already exists, the template
345    /// does not exist, the template has active connections, or if the
346    /// connection fails.
347    ///
348    /// # Examples
349    ///
350    /// ```no_run
351    /// use pg_embedded_setup_unpriv::TestCluster;
352    ///
353    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
354    /// let cluster = TestCluster::new()?;
355    ///
356    /// // Create and migrate a template once
357    /// cluster.ensure_template_exists("migrated_template", |_| Ok(()))?;
358    ///
359    /// // Each test gets its own database cloned from the template
360    /// let temp_db = cluster.connection()
361    ///     .temporary_database_from_template("test_db", "migrated_template")?;
362    ///
363    /// // Database is dropped automatically when temp_db goes out of scope
364    /// # Ok(())
365    /// # }
366    /// ```
367    pub fn temporary_database_from_template(
368        &self,
369        name: impl Into<DatabaseName>,
370        template: impl Into<DatabaseName>,
371    ) -> BootstrapResult<TemporaryDatabase> {
372        let db_name = name.into();
373        let template_name = template.into();
374        let _span =
375            info_span!("temporary_database_from_template", db = %db_name.as_str(), template = %template_name.as_str())
376                .entered();
377        self.create_database_from_template(db_name.as_str(), template_name.as_str())?;
378        Ok(TemporaryDatabase::new(
379            db_name.as_str().to_owned(),
380            self.database_url("postgres"),
381            self.database_url(db_name.as_str()),
382        ))
383    }
384}