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