Skip to main content

pg_embedded_setup_unpriv/
error.rs

1//! Domain error types for the embedded `PostgreSQL` bootstrapper.
2
3use color_eyre::Report;
4use thiserror::Error;
5
6/// Result alias for operations that may return a [`PgEmbeddedError`].
7pub type Result<T> = std::result::Result<T, PgEmbeddedError>;
8
9/// Result alias for bootstrap-specific fallible operations.
10pub type BootstrapResult<T> = std::result::Result<T, BootstrapError>;
11
12/// Result alias for privilege-management fallible operations.
13pub type PrivilegeResult<T> = std::result::Result<T, PrivilegeError>;
14
15/// Result alias for configuration fallible operations.
16pub type ConfigResult<T> = std::result::Result<T, ConfigError>;
17
18/// Top-level error exposed by the crate.
19#[derive(Debug, Error)]
20pub enum PgEmbeddedError {
21    /// Indicates bootstrap initialization failed.
22    #[error("bootstrap failed: {0}")]
23    Bootstrap(#[from] BootstrapError),
24    /// Indicates privilege management failed.
25    #[error("privilege management failed: {0}")]
26    Privilege(#[from] PrivilegeError),
27    /// Indicates configuration parsing failed.
28    #[error("configuration parsing failed: {0}")]
29    Config(#[from] ConfigError),
30}
31
32/// Categorizes bootstrap failures so callers can branch on structured errors.
33#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
34pub enum BootstrapErrorKind {
35    /// Represents errors without a more specific semantic meaning.
36    #[default]
37    Other,
38    /// Indicates the configured worker binary is missing from disk.
39    WorkerBinaryMissing,
40    /// Indicates a PATH entry used for worker discovery is not valid UTF-8.
41    WorkerBinaryPathNonUtf8,
42    /// The data directory holds a cluster but its password file does not exist.
43    ClusterPasswordMissing,
44    /// The data directory holds a cluster but its password file, or the
45    /// cluster marker itself, cannot be read.
46    ClusterPasswordUnreadable,
47    /// The data directory holds a cluster but its password file is empty.
48    ClusterPasswordEmpty,
49    /// `PG_EXTENSIONS*` is set but incomplete or malformed (no manifest, a
50    /// URL manifest without a digest, a bad name, or a bad digest).
51    ///
52    /// The digest is required for every URL manifest, loopback `http://`
53    /// included, and optional for a filesystem path.
54    ExtensionConfigInvalid,
55    /// The extension manifest could not be read from its path or URL.
56    ExtensionManifestUnavailable,
57    /// The manifest bytes do not hash to `PG_EXTENSIONS_MANIFEST_SHA256`.
58    ExtensionManifestDigestMismatch,
59    /// The manifest is not valid JSON, has the wrong schema version, or is
60    /// missing a field.
61    ExtensionManifestInvalid,
62    /// The manifest offers no artifact for the requested name, the running
63    /// `PostgreSQL` major and minor, and the compile target.
64    ExtensionUnavailable,
65    /// The extension archive could not be downloaded and no cached copy exists.
66    ExtensionArchiveUnavailable,
67    /// The archive bytes do not hash to the digest recorded in the manifest.
68    ExtensionArchiveDigestMismatch,
69    /// The archive contains a forbidden entry, escapes its prefixes, or does
70    /// not match the file list recorded in the manifest.
71    ExtensionArchiveInvalid,
72    /// Writing the extension files into the installation tree failed.
73    ExtensionInstallFailed,
74}
75
76/// Captures bootstrap-specific failures.
77#[derive(Debug, Error)]
78#[error("{report}")]
79pub struct BootstrapError {
80    kind: BootstrapErrorKind,
81    #[source]
82    report: Report,
83}
84
85impl BootstrapError {
86    /// Constructs a new bootstrap error with the provided kind and diagnostic
87    /// report.
88    #[must_use]
89    pub const fn new(kind: BootstrapErrorKind, report: Report) -> Self { Self { kind, report } }
90
91    /// Returns the semantic category for this bootstrap failure.
92    #[must_use]
93    pub const fn kind(&self) -> BootstrapErrorKind { self.kind }
94
95    /// Extracts the underlying diagnostic report.
96    pub fn into_report(self) -> Report { self.report }
97}
98
99impl From<Report> for BootstrapError {
100    fn from(report: Report) -> Self { Self::new(BootstrapErrorKind::Other, report) }
101}
102
103impl From<PrivilegeError> for BootstrapError {
104    fn from(err: PrivilegeError) -> Self {
105        let PrivilegeError(report) = err;
106        Self::new(BootstrapErrorKind::Other, report)
107    }
108}
109
110impl From<ConfigError> for BootstrapError {
111    fn from(err: ConfigError) -> Self {
112        let ConfigError(report) = err;
113        Self::new(BootstrapErrorKind::Other, report)
114    }
115}
116
117impl From<PgEmbeddedError> for BootstrapError {
118    fn from(err: PgEmbeddedError) -> Self {
119        match err {
120            PgEmbeddedError::Bootstrap(inner) => inner,
121            PgEmbeddedError::Privilege(inner) => inner.into(),
122            PgEmbeddedError::Config(inner) => inner.into(),
123        }
124    }
125}
126
127/// Captures privilege-management failures.
128#[derive(Debug, Error)]
129#[error(transparent)]
130pub struct PrivilegeError(#[from] Report);
131
132/// Captures configuration failures.
133#[derive(Debug, Error)]
134#[error(transparent)]
135pub struct ConfigError(#[from] Report);
136
137#[cfg(test)]
138mod tests {
139    //! Unit tests for error display formats.
140
141    use color_eyre::eyre::eyre;
142    use rstest::rstest;
143
144    use super::*;
145
146    #[rstest]
147    #[case::bootstrap(
148        "PG_EMBEDDED_WORKER must be set",
149        "bootstrap failed:",
150        |msg: &str| PgEmbeddedError::Bootstrap(BootstrapError::from(eyre!("{}", msg)))
151    )]
152    #[case::privilege(
153        "failed to drop privileges",
154        "privilege management failed:",
155        |msg: &str| PgEmbeddedError::Privilege(PrivilegeError::from(eyre!("{}", msg)))
156    )]
157    #[case::config(
158        "invalid port number",
159        "configuration parsing failed:",
160        |msg: &str| PgEmbeddedError::Config(ConfigError::from(eyre!("{}", msg)))
161    )]
162    fn pg_embedded_error_includes_inner_message(
163        #[case] inner_message: &str,
164        #[case] expected_prefix: &str,
165        #[case] constructor: fn(&str) -> PgEmbeddedError,
166    ) {
167        let pg_err = constructor(inner_message);
168
169        let display = pg_err.to_string();
170
171        assert!(
172            display.contains(expected_prefix),
173            "expected '{expected_prefix}' prefix, got: {display}"
174        );
175        assert!(
176            display.contains(inner_message),
177            "expected inner message '{inner_message}' in display, got: {display}"
178        );
179    }
180
181    #[test]
182    fn bootstrap_error_displays_report_message() {
183        let inner_message = "database connection failed";
184        let err = BootstrapError::from(eyre!(inner_message));
185
186        let display = err.to_string();
187
188        assert!(
189            display.contains(inner_message),
190            "expected '{inner_message}' in display, got: {display}"
191        );
192    }
193
194    #[test]
195    fn bootstrap_error_kind_defaults_to_other() {
196        assert_eq!(BootstrapErrorKind::default(), BootstrapErrorKind::Other);
197    }
198
199    #[test]
200    fn bootstrap_error_preserves_kind_and_into_report() {
201        let err = BootstrapError::new(
202            BootstrapErrorKind::WorkerBinaryMissing,
203            eyre!("worker gone"),
204        );
205        assert_eq!(err.kind(), BootstrapErrorKind::WorkerBinaryMissing);
206        assert!(err.into_report().to_string().contains("worker gone"));
207    }
208
209    #[rstest]
210    #[case::config(BootstrapErrorKind::ExtensionConfigInvalid)]
211    #[case::manifest_unavailable(BootstrapErrorKind::ExtensionManifestUnavailable)]
212    #[case::manifest_digest(BootstrapErrorKind::ExtensionManifestDigestMismatch)]
213    #[case::manifest_invalid(BootstrapErrorKind::ExtensionManifestInvalid)]
214    #[case::unavailable(BootstrapErrorKind::ExtensionUnavailable)]
215    #[case::archive_unavailable(BootstrapErrorKind::ExtensionArchiveUnavailable)]
216    #[case::archive_digest(BootstrapErrorKind::ExtensionArchiveDigestMismatch)]
217    #[case::archive_invalid(BootstrapErrorKind::ExtensionArchiveInvalid)]
218    #[case::install_failed(BootstrapErrorKind::ExtensionInstallFailed)]
219    fn extension_error_kinds_survive_wrapping(#[case] kind: BootstrapErrorKind) {
220        let err = BootstrapError::new(kind, eyre!("extension detail"));
221        assert_eq!(err.kind(), kind);
222        let wrapped = BootstrapError::from(PgEmbeddedError::Bootstrap(err));
223        assert_eq!(wrapped.kind(), kind);
224        assert!(wrapped.to_string().contains("extension detail"));
225    }
226
227    #[test]
228    fn bootstrap_error_from_privilege_error_is_other() {
229        let privilege = PrivilegeError::from(eyre!("no privileges"));
230        let err = BootstrapError::from(privilege);
231        assert_eq!(err.kind(), BootstrapErrorKind::Other);
232        assert!(err.to_string().contains("no privileges"));
233    }
234
235    #[test]
236    fn bootstrap_error_from_config_error_is_other() {
237        let config = ConfigError::from(eyre!("bad config"));
238        let err = BootstrapError::from(config);
239        assert_eq!(err.kind(), BootstrapErrorKind::Other);
240        assert!(err.to_string().contains("bad config"));
241    }
242
243    #[rstest]
244    #[case::bootstrap(
245        |report| PgEmbeddedError::Bootstrap(BootstrapError::new(
246            BootstrapErrorKind::WorkerBinaryPathNonUtf8,
247            report,
248        )),
249        BootstrapErrorKind::WorkerBinaryPathNonUtf8,
250    )]
251    #[case::privilege(
252        |report| PgEmbeddedError::Privilege(PrivilegeError::from(report)),
253        BootstrapErrorKind::Other,
254    )]
255    #[case::config(
256        |report| PgEmbeddedError::Config(ConfigError::from(report)),
257        BootstrapErrorKind::Other,
258    )]
259    fn bootstrap_error_from_pg_embedded_error_maps_each_variant(
260        #[case] constructor: fn(Report) -> PgEmbeddedError,
261        #[case] expected_kind: BootstrapErrorKind,
262    ) {
263        let pg_err = constructor(eyre!("inner detail"));
264        let err = BootstrapError::from(pg_err);
265        assert_eq!(err.kind(), expected_kind);
266        assert!(err.to_string().contains("inner detail"));
267    }
268}