Skip to main content

pg_embedded_setup_unpriv/cluster/
handle.rs

1//! Send-safe handle for accessing a running `PostgreSQL` cluster.
2//!
3//! [`ClusterHandle`] provides thread-safe access to cluster metadata and
4//! connection helpers. Unlike [`TestCluster`](super::TestCluster), handles
5//! implement [`Send`] and [`Sync`], enabling patterns such as:
6//!
7//! - Shared cluster fixtures using [`OnceLock`](std::sync::OnceLock)
8//! - rstest fixtures with timeouts (which require `Send + 'static`)
9//! - Cross-thread sharing in async test patterns
10//!
11//! # Architecture
12//!
13//! The handle/guard split separates concerns:
14//!
15//! - **`ClusterHandle`**: Read-only access to cluster metadata. `Send + Sync`.
16//! - **`ClusterGuard`**: Manages environment and shutdown. `!Send`.
17//!
18//! This separation preserves the safety of thread-local environment management
19//! whilst enabling the most common shared cluster use cases.
20//!
21//! # Examples
22//!
23//! ```no_run
24//! use std::sync::OnceLock;
25//! use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
26//!
27//! static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
28//!
29//! fn shared_handle() -> &'static ClusterHandle {
30//!     SHARED.get_or_init(|| {
31//!         let (handle, _guard) = TestCluster::new_split()
32//!             .expect("cluster bootstrap failed");
33//!         // Guard drops here, but cluster keeps running
34//!         handle
35//!     })
36//! }
37//! ```
38
39use super::connection::TestClusterConnection;
40use super::lifecycle::DatabaseName;
41use super::temporary_database::TemporaryDatabase;
42use crate::error::BootstrapResult;
43use crate::{TestBootstrapEnvironment, TestBootstrapSettings};
44use postgresql_embedded::Settings;
45
46/// Send-safe handle providing read-only access to a running `PostgreSQL` cluster.
47///
48/// Handles are lightweight and cloneable. They contain only the bootstrap
49/// metadata needed to construct connections and query cluster state.
50///
51/// # Thread Safety
52///
53/// `ClusterHandle` implements [`Send`] and [`Sync`], making it safe to share
54/// across threads. The underlying `PostgreSQL` process is an external OS process
55/// that handles concurrent connections safely.
56///
57/// # Obtaining a Handle
58///
59/// Use [`TestCluster::new_split()`](super::TestCluster::new_split) to obtain
60/// a handle and guard pair:
61///
62/// ```no_run
63/// use pg_embedded_setup_unpriv::TestCluster;
64///
65/// let (handle, guard) = TestCluster::new_split()?;
66/// // handle: ClusterHandle (Send + Sync)
67/// // guard: ClusterGuard (!Send, manages lifecycle)
68/// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
69/// ```
70#[derive(Debug, Clone)]
71pub struct ClusterHandle {
72    bootstrap: TestBootstrapSettings,
73}
74
75// Compile-time assertions that ClusterHandle is Send + Sync.
76const _: () = {
77    const fn assert_send<T: Send>() {}
78    const fn assert_sync<T: Sync>() {}
79    assert_send::<ClusterHandle>();
80    assert_sync::<ClusterHandle>();
81};
82
83impl From<TestBootstrapSettings> for ClusterHandle {
84    fn from(bootstrap: TestBootstrapSettings) -> Self {
85        Self { bootstrap }
86    }
87}
88
89impl ClusterHandle {
90    /// Creates a new handle from bootstrap settings.
91    pub(super) const fn new(bootstrap: TestBootstrapSettings) -> Self {
92        Self { bootstrap }
93    }
94
95    /// Returns the prepared `PostgreSQL` settings for the running cluster.
96    ///
97    /// # Examples
98    ///
99    /// ```no_run
100    /// use pg_embedded_setup_unpriv::TestCluster;
101    ///
102    /// let (handle, _guard) = TestCluster::new_split()?;
103    /// let url = handle.settings().url("my_database");
104    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
105    /// ```
106    #[must_use]
107    pub const fn settings(&self) -> &Settings {
108        &self.bootstrap.settings
109    }
110
111    /// Returns the environment required for clients to interact with the cluster.
112    ///
113    /// # Examples
114    ///
115    /// ```no_run
116    /// use pg_embedded_setup_unpriv::TestCluster;
117    ///
118    /// let (handle, _guard) = TestCluster::new_split()?;
119    /// let env = handle.environment();
120    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
121    /// ```
122    #[must_use]
123    pub const fn environment(&self) -> &TestBootstrapEnvironment {
124        &self.bootstrap.environment
125    }
126
127    /// Returns the bootstrap metadata captured when the cluster was started.
128    ///
129    /// # Examples
130    ///
131    /// ```no_run
132    /// use pg_embedded_setup_unpriv::TestCluster;
133    ///
134    /// let (handle, _guard) = TestCluster::new_split()?;
135    /// let bootstrap = handle.bootstrap();
136    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
137    /// ```
138    #[must_use]
139    pub const fn bootstrap(&self) -> &TestBootstrapSettings {
140        &self.bootstrap
141    }
142
143    /// Returns helper methods for constructing connection artefacts.
144    ///
145    /// # Examples
146    ///
147    /// ```no_run
148    /// use pg_embedded_setup_unpriv::TestCluster;
149    ///
150    /// let (handle, _guard) = TestCluster::new_split()?;
151    /// let metadata = handle.connection().metadata();
152    /// println!(
153    ///     "postgresql://{}:***@{}:{}/postgres",
154    ///     metadata.superuser(),
155    ///     metadata.host(),
156    ///     metadata.port(),
157    /// );
158    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
159    /// ```
160    #[must_use]
161    pub fn connection(&self) -> TestClusterConnection {
162        TestClusterConnection::new(&self.bootstrap)
163    }
164}
165
166// Delegation methods that forward to TestClusterConnection.
167impl ClusterHandle {
168    /// Creates a new database with the given name.
169    ///
170    /// See [`TestClusterConnection::create_database`] for details.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the database already exists or if the connection fails.
175    pub fn create_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
176        self.connection().create_database(name)
177    }
178
179    /// Creates a new database by cloning an existing template.
180    ///
181    /// See [`TestClusterConnection::create_database_from_template`] for details.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the target database already exists, the template does
186    /// not exist, or the connection fails.
187    pub fn create_database_from_template(
188        &self,
189        name: impl Into<DatabaseName>,
190        template: impl Into<DatabaseName>,
191    ) -> BootstrapResult<()> {
192        self.connection()
193            .create_database_from_template(name, template)
194    }
195
196    /// Drops an existing database.
197    ///
198    /// See [`TestClusterConnection::drop_database`] for details.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the database does not exist or the connection fails.
203    pub fn drop_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
204        self.connection().drop_database(name)
205    }
206
207    /// Checks whether a database with the given name exists.
208    ///
209    /// See [`TestClusterConnection::database_exists`] for details.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if the connection fails.
214    pub fn database_exists(&self, name: impl Into<DatabaseName>) -> BootstrapResult<bool> {
215        self.connection().database_exists(name)
216    }
217
218    /// Ensures a template database exists, creating it if necessary.
219    ///
220    /// See [`TestClusterConnection::ensure_template_exists`] for details.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if database creation fails or if `setup_fn` returns an error.
225    pub fn ensure_template_exists<F>(
226        &self,
227        name: impl Into<DatabaseName>,
228        setup_fn: F,
229    ) -> BootstrapResult<()>
230    where
231        F: FnOnce(&str) -> BootstrapResult<()>,
232    {
233        self.connection().ensure_template_exists(name, setup_fn)
234    }
235
236    /// Creates a temporary database that is dropped when the guard is dropped.
237    ///
238    /// See [`TestClusterConnection::temporary_database`] for details.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if the database already exists or the connection fails.
243    pub fn temporary_database(
244        &self,
245        name: impl Into<DatabaseName>,
246    ) -> BootstrapResult<TemporaryDatabase> {
247        self.connection().temporary_database(name)
248    }
249
250    /// Creates a temporary database from a template.
251    ///
252    /// See [`TestClusterConnection::temporary_database_from_template`] for details.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the target database already exists, the template does
257    /// not exist, or the connection fails.
258    pub fn temporary_database_from_template(
259        &self,
260        name: impl Into<DatabaseName>,
261        template: impl Into<DatabaseName>,
262    ) -> BootstrapResult<TemporaryDatabase> {
263        self.connection()
264            .temporary_database_from_template(name, template)
265    }
266}