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}