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}