Skip to main content

pg_embedded_setup_unpriv/extensions/manifest/
select.rs

1//! Choosing one artefact for the running server and compile target.
2//!
3//! Split from the schema half because it answers a different question.
4//! Parsing asks whether the manifest is well formed; this asks which of its
5//! rows describes the server about to start, and the answer is deliberately
6//! narrower than what would load.
7
8use color_eyre::eyre::{Report, eyre};
9use postgresql_embedded::Version;
10
11use super::{
12    Manifest,
13    ManifestArtifact,
14    ManifestExtension,
15    ManifestSource,
16    artifact_version,
17    unavailable,
18};
19use crate::{error::BootstrapResult, extensions::ExtensionName};
20
21/// What to look for in a manifest: one name for one running server and target.
22#[derive(Debug, Clone, Copy)]
23pub struct ArtifactQuery<'a> {
24    /// Requested `CREATE EXTENSION` name.
25    pub name: &'a ExtensionName,
26    /// Version of the `PostgreSQL` installed in the tree.
27    pub running: &'a Version,
28    /// Compile target triple.
29    pub target: &'a str,
30}
31
32/// The extension and artefact chosen for a request.
33#[derive(Debug, Clone, Copy)]
34pub struct Selection<'a> {
35    /// The extension entry the artefact belongs to.
36    pub extension: &'a ManifestExtension,
37    /// The artefact to install.
38    pub artifact: &'a ManifestArtifact,
39}
40
41impl Manifest {
42    /// Selects the artefact for `name` matching the running `PostgreSQL` major
43    /// and minor and the compile `target`.
44    ///
45    /// Theseus's third version component is a build number rather than a
46    /// `PostgreSQL` release, so it is not compared. There is no cross-minor
47    /// fallback: see [`ArtifactQuery`].
48    ///
49    /// # Errors
50    ///
51    /// Returns `ExtensionUnavailable` when no artefact matches; the message
52    /// lists what the manifest offers for that name.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use camino::Utf8PathBuf;
58    /// use pg_embedded_setup_unpriv::extensions::{
59    ///     ArtifactQuery,
60    ///     ExtensionName,
61    ///     Manifest,
62    ///     ManifestSource,
63    /// };
64    /// use postgresql_embedded::Version;
65    ///
66    /// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
67    /// let manifest = Manifest::parse(MANIFEST.as_bytes())?;
68    /// let name = ExtensionName::new("vector")?;
69    /// let source = ManifestSource::Path {
70    ///     path: Utf8PathBuf::from("/srv/extensions/manifest.json"),
71    ///     sha256: None,
72    /// };
73    ///
74    /// let running = Version::new(17, 11, 0);
75    /// let chosen = manifest.select(
76    ///     ArtifactQuery {
77    ///         name: &name,
78    ///         running: &running,
79    ///         target: "x86_64-unknown-linux-gnu",
80    ///     },
81    ///     &source,
82    /// )?;
83    /// assert_eq!(chosen.artifact.postgresql, "17.11.0");
84    ///
85    /// // A neighbouring minor is a miss, not a fallback: the manifest pins one
86    /// // digest per version, and 17.10 is a different row.
87    /// let other = Version::new(17, 10, 0);
88    /// assert!(
89    ///     manifest
90    ///         .select(
91    ///             ArtifactQuery {
92    ///                 name: &name,
93    ///                 running: &other,
94    ///                 target: "x86_64-unknown-linux-gnu"
95    ///             },
96    ///             &source,
97    ///         )
98    ///         .is_err()
99    /// );
100    /// # Ok(())
101    /// # }
102    /// # const MANIFEST: &str = r#"{
103    /// #   "schema_version": 1,
104    /// #   "release": "v1.0.0",
105    /// #   "generated_at": "2026-09-05T00:00:00+00:00",
106    /// #   "extensions": [{
107    /// #     "name": "vector",
108    /// #     "package": "pgvector",
109    /// #     "version": "0.8.6",
110    /// #     "source": {
111    /// #       "repository": "https://github.com/pgvector/pgvector",
112    /// #       "tag": "v0.8.6",
113    /// #       "commit": "0123456789abcdef0123456789abcdef01234567"
114    /// #     },
115    /// #     "artifacts": [{
116    /// #       "postgresql": "17.11.0",
117    /// #       "target": "x86_64-unknown-linux-gnu",
118    /// #       "file": "vector.tar.gz",
119    /// #       "url": "https://example.invalid/vector.tar.gz",
120    /// #       "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
121    /// #       "size": 1024,
122    /// #       "files": ["lib/vector.so"]
123    /// #     }]
124    /// #   }]
125    /// # }"#;
126    /// ```
127    pub fn select<'a>(
128        &'a self,
129        query: ArtifactQuery<'_>,
130        source: &ManifestSource,
131    ) -> BootstrapResult<Selection<'a>> {
132        let ArtifactQuery {
133            name,
134            running,
135            target,
136        } = query;
137        let Some(extension) = self
138            .extensions
139            .iter()
140            .find(|extension| extension.name == name.as_str())
141        else {
142            return Err(unavailable(eyre!(
143                "manifest at {} lists no extension named {name}; it offers: {}",
144                source.location(),
145                self.extension_names().join(", ")
146            )));
147        };
148        let artifact = extension
149            .artifacts
150            .iter()
151            .find(|artifact| artifact_matches(artifact, running, target))
152            .ok_or_else(|| unavailable(no_artifact_report(extension, running, target, source)))?;
153        Ok(Selection {
154            extension,
155            artifact,
156        })
157    }
158}
159
160/// Both components of the `PostgreSQL` version must match, and the compile
161/// target with them.
162///
163/// An archive built for one major would in fact load into every minor of that
164/// major: the server's `Pg_magic_func` block checks the major and the layout
165/// constants, not the minor. Selection is narrower than loading on purpose.
166/// The manifest pins one digest per (name, version, target) row, so accepting
167/// a neighbouring minor would install bytes the consumer's pinned manifest
168/// digest does not describe for the server actually running, and the exactness
169/// of the chain is the point of the hook. Issue #222 states the rule and puts
170/// cross-minor fallback out of scope, behind an explicit opt-in if the estate
171/// ever wants it.
172///
173/// Theseus's third component is a build number rather than a `PostgreSQL`
174/// release, so it is not compared.
175fn artifact_matches(artifact: &ManifestArtifact, running: &Version, target: &str) -> bool {
176    artifact_version(artifact).is_some_and(|built| {
177        built.major == running.major && built.minor == running.minor && artifact.target == target
178    })
179}
180
181/// Builds the `ExtensionUnavailable` message listing what the manifest offers.
182fn no_artifact_report(
183    extension: &ManifestExtension,
184    running: &Version,
185    target: &str,
186    source: &ManifestSource,
187) -> Report {
188    let offered: Vec<String> = extension
189        .artifacts
190        .iter()
191        .map(|artifact| format!("{} on {}", artifact.postgresql, artifact.target))
192        .collect();
193    eyre!(
194        "manifest at {} has no {} archive for PostgreSQL {}.{} on {target}; it offers: {}",
195        source.location(),
196        extension.name,
197        running.major,
198        running.minor,
199        if offered.is_empty() {
200            "nothing".to_owned()
201        } else {
202            offered.join(", ")
203        }
204    )
205}