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