Skip to main content

pg_embedded_setup_unpriv/cluster/
connection.rs

1//! Connection helpers for `TestCluster`, including metadata accessors and optional Diesel support.
2
3use camino::{Utf8Path, Utf8PathBuf};
4#[cfg(feature = "diesel-support")]
5use color_eyre::eyre::WrapErr;
6use color_eyre::eyre::eyre;
7use postgres::{Client, NoTls};
8use postgresql_embedded::Settings;
9
10use crate::TestBootstrapSettings;
11use crate::error::BootstrapResult;
12
13/// Escapes a SQL identifier by doubling embedded double quotes.
14///
15/// `PostgreSQL` identifiers are quoted with double quotes. Any embedded
16/// double quote must be escaped by doubling it.
17pub(crate) fn escape_identifier(name: &str) -> String {
18    name.replace('"', "\"\"")
19}
20
21/// Creates a new `PostgreSQL` client connection from the given URL.
22///
23/// This is a shared helper for admin database connections used by both
24/// `TestClusterConnection` and `TemporaryDatabase`.
25pub(crate) fn connect_admin(url: &str) -> BootstrapResult<Client> {
26    Client::connect(url, NoTls).map_err(|err| {
27        crate::error::BootstrapError::from(eyre!("failed to connect to admin database: {err}"))
28    })
29}
30
31/// Provides ergonomic accessors for connection-oriented cluster metadata.
32///
33/// # Examples
34/// ```no_run
35/// use pg_embedded_setup_unpriv::TestCluster;
36///
37/// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
38/// let cluster = TestCluster::new()?;
39/// let metadata = cluster.connection().metadata();
40/// assert_eq!(metadata.host(), "localhost");
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Debug, Clone)]
45pub struct ConnectionMetadata {
46    settings: Settings,
47    pgpass_file: Utf8PathBuf,
48}
49
50impl ConnectionMetadata {
51    pub(crate) fn from_settings(settings: &TestBootstrapSettings) -> Self {
52        Self {
53            settings: settings.settings.clone(),
54            pgpass_file: settings.environment.pgpass_file.clone(),
55        }
56    }
57
58    /// Returns the configured database host.
59    #[must_use]
60    pub fn host(&self) -> &str {
61        self.settings.host.as_str()
62    }
63
64    /// Returns the configured port.
65    #[must_use]
66    pub const fn port(&self) -> u16 {
67        self.settings.port
68    }
69
70    /// Returns the configured superuser name.
71    #[must_use]
72    pub fn superuser(&self) -> &str {
73        self.settings.username.as_str()
74    }
75
76    /// Returns the generated superuser password.
77    #[must_use]
78    pub fn password(&self) -> &str {
79        self.settings.password.as_str()
80    }
81
82    /// Returns the prepared `.pgpass` file path.
83    #[must_use]
84    pub fn pgpass_file(&self) -> &Utf8Path {
85        self.pgpass_file.as_ref()
86    }
87
88    /// Constructs a libpq-compatible URL for `database` using the underlying
89    /// `postgresql_embedded` helper.
90    #[must_use]
91    pub fn database_url(&self, database: &str) -> String {
92        self.settings.url(database)
93    }
94}
95
96/// Accessor for connection helpers derived from a
97/// [`TestCluster`](crate::TestCluster).
98///
99/// Enable the `diesel-support` feature to call the Diesel connection helper.
100///
101/// # Examples
102/// ```no_run
103/// use pg_embedded_setup_unpriv::TestCluster;
104///
105/// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
106/// let cluster = TestCluster::new()?;
107/// let url = cluster.connection().database_url("postgres");
108/// assert!(url.contains("postgresql://"));
109/// # Ok(())
110/// # }
111/// ```
112#[derive(Debug, Clone)]
113pub struct TestClusterConnection {
114    metadata: ConnectionMetadata,
115}
116
117impl TestClusterConnection {
118    pub(crate) fn new(settings: &TestBootstrapSettings) -> Self {
119        Self {
120            metadata: ConnectionMetadata::from_settings(settings),
121        }
122    }
123
124    /// Returns host metadata without exposing internal storage.
125    #[must_use]
126    pub fn host(&self) -> &str {
127        self.metadata.host()
128    }
129
130    /// Returns the configured port.
131    #[must_use]
132    pub const fn port(&self) -> u16 {
133        self.metadata.port()
134    }
135
136    /// Returns the configured superuser account name.
137    #[must_use]
138    pub fn superuser(&self) -> &str {
139        self.metadata.superuser()
140    }
141
142    /// Returns the generated password for the superuser.
143    #[must_use]
144    pub fn password(&self) -> &str {
145        self.metadata.password()
146    }
147
148    /// Returns the `.pgpass` file prepared during bootstrap.
149    #[must_use]
150    pub fn pgpass_file(&self) -> &Utf8Path {
151        self.metadata.pgpass_file()
152    }
153
154    /// Provides an owned snapshot of the connection metadata.
155    #[must_use]
156    pub fn metadata(&self) -> ConnectionMetadata {
157        self.metadata.clone()
158    }
159
160    /// Builds a libpq-compatible database URL for `database`.
161    #[must_use]
162    pub fn database_url(&self, database: &str) -> String {
163        self.metadata.database_url(database)
164    }
165
166    /// Establishes a Diesel connection for the target `database`.
167    ///
168    /// # Errors
169    /// Returns a [`crate::error::BootstrapError`] when Diesel cannot connect.
170    #[cfg(feature = "diesel-support")]
171    pub fn diesel_connection(&self, database: &str) -> BootstrapResult<diesel::PgConnection> {
172        use diesel::Connection;
173
174        diesel::PgConnection::establish(&self.database_url(database))
175            .wrap_err(format!("failed to connect to {database} via Diesel"))
176            .map_err(crate::error::BootstrapError::from)
177    }
178
179    /// Connects to the `postgres` administration database.
180    pub(super) fn admin_client(&self) -> BootstrapResult<Client> {
181        connect_admin(&self.database_url("postgres"))
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::bootstrap::{ExecutionMode, ExecutionPrivileges, TestBootstrapEnvironment};
189    use crate::{CleanupMode, TestBootstrapSettings};
190    use postgresql_embedded::Settings;
191    use std::time::Duration;
192
193    fn sample_settings() -> TestBootstrapSettings {
194        let settings = Settings {
195            host: "127.0.0.1".into(),
196            port: 55_321,
197            username: "fixture_user".into(),
198            password: "fixture_pass".into(),
199            data_dir: "/tmp/cluster-data".into(),
200            installation_dir: "/tmp/cluster-install".into(),
201            ..Settings::default()
202        };
203
204        TestBootstrapSettings {
205            privileges: ExecutionPrivileges::Unprivileged,
206            execution_mode: ExecutionMode::InProcess,
207            settings,
208            environment: TestBootstrapEnvironment {
209                home: Utf8PathBuf::from("/tmp/home"),
210                xdg_cache_home: Utf8PathBuf::from("/tmp/home/cache"),
211                xdg_runtime_dir: Utf8PathBuf::from("/tmp/home/run"),
212                pgpass_file: Utf8PathBuf::from("/tmp/home/.pgpass"),
213                tz_dir: Some(Utf8PathBuf::from("/usr/share/zoneinfo")),
214                timezone: "UTC".into(),
215            },
216            worker_binary: None,
217            setup_timeout: Duration::from_secs(1),
218            start_timeout: Duration::from_secs(1),
219            shutdown_timeout: Duration::from_secs(1),
220            cleanup_mode: CleanupMode::default(),
221            binary_cache_dir: None,
222        }
223    }
224
225    #[test]
226    fn metadata_reflects_underlying_settings() {
227        let settings = sample_settings();
228        let connection = TestClusterConnection::new(&settings);
229        let metadata = connection.metadata();
230
231        assert_eq!(metadata.host(), "127.0.0.1");
232        assert_eq!(metadata.port(), 55_321);
233        assert_eq!(metadata.superuser(), "fixture_user");
234        assert_eq!(metadata.password(), "fixture_pass");
235        assert_eq!(metadata.pgpass_file(), Utf8Path::new("/tmp/home/.pgpass"));
236    }
237
238    #[test]
239    fn database_url_matches_postgresql_embedded() {
240        let settings = sample_settings();
241        let connection = TestClusterConnection::new(&settings);
242        let expected = settings.settings.url("postgres");
243
244        assert_eq!(connection.database_url("postgres"), expected);
245    }
246}