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//!
26//! use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
27//!
28//! static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
29//!
30//! fn shared_handle() -> &'static ClusterHandle {
31//!     SHARED.get_or_init(|| {
32//!         let (handle, guard) = TestCluster::new_split().expect("cluster bootstrap failed");
33//!         handle
34//!             .register_shutdown_on_exit()
35//!             .expect("shutdown hook registration failed");
36//!         std::mem::forget(guard);
37//!         handle
38//!     })
39//! }
40//! ```
41
42use postgresql_embedded::Settings;
43
44use super::{
45    connection::TestClusterConnection,
46    lifecycle::DatabaseName,
47    temporary_database::TemporaryDatabase,
48};
49use crate::{
50    TestBootstrapEnvironment,
51    TestBootstrapSettings,
52    error::BootstrapResult,
53    extensions::InstalledExtension,
54};
55
56/// Send-safe handle providing read-only access to a running `PostgreSQL` cluster.
57///
58/// Handles are lightweight and cloneable. They contain only the bootstrap
59/// metadata needed to construct connections and query cluster state.
60///
61/// # Thread Safety
62///
63/// `ClusterHandle` implements [`Send`] and [`Sync`], making it safe to share
64/// across threads. The underlying `PostgreSQL` process is an external OS process
65/// that handles concurrent connections safely.
66///
67/// # Obtaining a Handle
68///
69/// Use [`TestCluster::new_split()`](super::TestCluster::new_split) to obtain
70/// a handle and guard pair:
71///
72/// ```no_run
73/// use pg_embedded_setup_unpriv::TestCluster;
74///
75/// let (handle, guard) = TestCluster::new_split()?;
76/// // handle: ClusterHandle (Send + Sync)
77/// // guard: ClusterGuard (!Send, manages lifecycle)
78/// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
79/// ```
80#[derive(Debug, Clone)]
81pub struct ClusterHandle {
82    bootstrap: TestBootstrapSettings,
83}
84
85// Compile-time assertions that ClusterHandle is Send + Sync.
86const _: () = {
87    const fn assert_send<T: Send>() {}
88    const fn assert_sync<T: Sync>() {}
89    assert_send::<ClusterHandle>();
90    assert_sync::<ClusterHandle>();
91};
92
93impl From<TestBootstrapSettings> for ClusterHandle {
94    fn from(bootstrap: TestBootstrapSettings) -> Self { Self { bootstrap } }
95}
96
97impl ClusterHandle {
98    /// Creates a new handle from bootstrap settings.
99    pub(super) const fn new(bootstrap: TestBootstrapSettings) -> Self { Self { bootstrap } }
100
101    /// Returns the prepared `PostgreSQL` settings for the running cluster.
102    ///
103    /// # Examples
104    ///
105    /// ```no_run
106    /// use pg_embedded_setup_unpriv::TestCluster;
107    ///
108    /// let (handle, _guard) = TestCluster::new_split()?;
109    /// let url = handle.settings().url("my_database");
110    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
111    /// ```
112    #[must_use]
113    pub const fn settings(&self) -> &Settings { &self.bootstrap.settings }
114
115    /// Returns the environment required for clients to interact with the cluster.
116    ///
117    /// # Examples
118    ///
119    /// ```no_run
120    /// use pg_embedded_setup_unpriv::TestCluster;
121    ///
122    /// let (handle, _guard) = TestCluster::new_split()?;
123    /// let env = handle.environment();
124    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
125    /// ```
126    #[must_use]
127    pub const fn environment(&self) -> &TestBootstrapEnvironment { &self.bootstrap.environment }
128
129    /// Returns the bootstrap metadata captured when the cluster was started.
130    ///
131    /// # Examples
132    ///
133    /// ```no_run
134    /// use pg_embedded_setup_unpriv::TestCluster;
135    ///
136    /// let (handle, _guard) = TestCluster::new_split()?;
137    /// let bootstrap = handle.bootstrap();
138    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
139    /// ```
140    #[must_use]
141    pub const fn bootstrap(&self) -> &TestBootstrapSettings { &self.bootstrap }
142
143    /// Returns what the extension hook installed before the server started.
144    ///
145    /// Empty when no extensions were declared through `PG_EXTENSIONS`.
146    ///
147    /// # Examples
148    ///
149    /// ```no_run
150    /// use pg_embedded_setup_unpriv::TestCluster;
151    ///
152    /// let (handle, _guard) = TestCluster::new_split()?;
153    /// for extension in handle.installed_extensions() {
154    ///     println!("{} {}", extension.name, extension.version);
155    /// }
156    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
157    /// ```
158    #[must_use]
159    pub fn installed_extensions(&self) -> &[InstalledExtension] {
160        &self.bootstrap.installed_extensions
161    }
162
163    /// Returns helper methods for constructing connection artefacts.
164    ///
165    /// # Examples
166    ///
167    /// ```no_run
168    /// use pg_embedded_setup_unpriv::TestCluster;
169    ///
170    /// let (handle, _guard) = TestCluster::new_split()?;
171    /// let metadata = handle.connection().metadata();
172    /// println!(
173    ///     "postgresql://{}:***@{}:{}/postgres",
174    ///     metadata.superuser(),
175    ///     metadata.host(),
176    ///     metadata.port(),
177    /// );
178    /// # Ok::<(), pg_embedded_setup_unpriv::BootstrapError>(())
179    /// ```
180    #[must_use]
181    pub fn connection(&self) -> TestClusterConnection {
182        TestClusterConnection::new(&self.bootstrap)
183    }
184}
185
186// Delegation methods that forward to TestClusterConnection.
187impl ClusterHandle {
188    /// Creates a new database with the given name.
189    ///
190    /// See [`TestClusterConnection::create_database`] for details.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if the database already exists or if the connection fails.
195    pub fn create_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
196        self.connection().create_database(name)
197    }
198
199    /// Creates a new database by cloning an existing template.
200    ///
201    /// See [`TestClusterConnection::create_database_from_template`] for details.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if the target database already exists, the template does
206    /// not exist, or the connection fails.
207    pub fn create_database_from_template(
208        &self,
209        name: impl Into<DatabaseName>,
210        template: impl Into<DatabaseName>,
211    ) -> BootstrapResult<()> {
212        self.connection()
213            .create_database_from_template(name, template)
214    }
215
216    /// Drops an existing database.
217    ///
218    /// See [`TestClusterConnection::drop_database`] for details.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the database does not exist or the connection fails.
223    pub fn drop_database(&self, name: impl Into<DatabaseName>) -> BootstrapResult<()> {
224        self.connection().drop_database(name)
225    }
226
227    /// Checks whether a database with the given name exists.
228    ///
229    /// See [`TestClusterConnection::database_exists`] for details.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the connection fails.
234    pub fn database_exists(&self, name: impl Into<DatabaseName>) -> BootstrapResult<bool> {
235        self.connection().database_exists(name)
236    }
237
238    /// Ensures a template database exists, creating it if necessary.
239    ///
240    /// See [`TestClusterConnection::ensure_template_exists`] for details.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if database creation fails or if `setup_fn` returns an error.
245    pub fn ensure_template_exists<F>(
246        &self,
247        name: impl Into<DatabaseName>,
248        setup_fn: F,
249    ) -> BootstrapResult<()>
250    where
251        F: FnOnce(&str) -> BootstrapResult<()>,
252    {
253        self.connection().ensure_template_exists(name, setup_fn)
254    }
255
256    /// Creates a temporary database that is dropped when the guard is dropped.
257    ///
258    /// See [`TestClusterConnection::temporary_database`] for details.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the database already exists or the connection fails.
263    pub fn temporary_database(
264        &self,
265        name: impl Into<DatabaseName>,
266    ) -> BootstrapResult<TemporaryDatabase> {
267        self.connection().temporary_database(name)
268    }
269
270    /// Creates a temporary database from a template.
271    ///
272    /// See [`TestClusterConnection::temporary_database_from_template`] for details.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the target database already exists, the template does
277    /// not exist, or the connection fails.
278    pub fn temporary_database_from_template(
279        &self,
280        name: impl Into<DatabaseName>,
281        template: impl Into<DatabaseName>,
282    ) -> BootstrapResult<TemporaryDatabase> {
283        self.connection()
284            .temporary_database_from_template(name, template)
285    }
286}
287
288// Process-exit shutdown hook registration.
289impl ClusterHandle {
290    /// Registers a process-exit hook that stops the `PostgreSQL` postmaster
291    /// when the process terminates.
292    ///
293    /// Intended for shared clusters where the [`ClusterGuard`](super::ClusterGuard)
294    /// is intentionally forgotten. The hook requests platform shutdown and
295    /// waits up to the configured shutdown timeout before escalating to the
296    /// platform's forceful termination mechanism.
297    ///
298    /// The method is idempotent: subsequent calls after the first
299    /// successful registration are no-ops. Only one cluster can be
300    /// tracked per process, matching the one-shared-cluster pattern.
301    ///
302    /// # Platform Support
303    ///
304    /// Supported on Unix (Linux, macOS) and Windows. Windows shutdown requests
305    /// terminate the postmaster process tree immediately because the process
306    /// exit hook cannot safely issue a graceful `PostgreSQL` shutdown command. On
307    /// other platforms this method is a silent no-op that returns `Ok(())`, so
308    /// callers need not gate on platform cfgs.
309    ///
310    /// # Errors
311    ///
312    /// Returns an error if `libc::atexit` registration fails on a supported
313    /// platform.
314    ///
315    /// # Examples
316    ///
317    /// ```no_run
318    /// use std::sync::OnceLock;
319    ///
320    /// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
321    ///
322    /// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
323    ///
324    /// fn shared_handle() -> &'static ClusterHandle {
325    ///     SHARED.get_or_init(|| {
326    ///         let (handle, guard) = TestCluster::new_split().expect("cluster bootstrap failed");
327    ///         handle
328    ///             .register_shutdown_on_exit()
329    ///             .expect("shutdown hook registration failed");
330    ///         std::mem::forget(guard);
331    ///         handle
332    ///     })
333    /// }
334    /// ```
335    pub fn register_shutdown_on_exit(&self) -> BootstrapResult<()> {
336        self.register_shutdown_on_exit_impl()
337    }
338
339    #[cfg(any(unix, windows))]
340    fn register_shutdown_on_exit_impl(&self) -> BootstrapResult<()> {
341        super::shutdown_hook::register_shutdown_hook(
342            self.bootstrap.settings.clone(),
343            self.bootstrap.shutdown_timeout,
344            self.bootstrap.cleanup_mode,
345        )
346    }
347
348    #[cfg(not(any(unix, windows)))]
349    fn register_shutdown_on_exit_impl(&self) -> BootstrapResult<()> {
350        // No-op on unsupported platforms. Unix and Windows both have concrete
351        // process-exit reapers; other targets can still use normal Drop-based
352        // cleanup.
353        Ok(())
354    }
355}