pgdo/version/
partial.rs

1use std::fmt;
2use std::str::FromStr;
3use std::{cmp::Ordering, sync::LazyLock};
4
5use regex::Regex;
6
7use super::{Version, VersionError};
8
9/// Represents a PostgreSQL version with some parts missing. This is the kind of
10/// thing we might find in a cluster's `PG_VERSION` file.
11///
12/// The "Version" column on the [PostgreSQL "Versioning Policy"
13/// page][versioning] is roughly what this models, but this can also optionally
14/// represent the "Current minor" column too.
15///
16/// [versioning]: https://www.postgresql.org/support/versioning/
17#[derive(Copy, Clone, Debug)]
18pub enum PartialVersion {
19    /// Pre-PostgreSQL 10, with major and point version numbers, e.g. 9.6. It is
20    /// an error to create this variant with a major number >= 10; see
21    /// [`checked`][`Self::checked`] for a way to guard against this.
22    Pre10m(u32, u32),
23    /// Pre-PostgreSQL 10, with major, point, and minor version numbers, e.g.
24    /// 9.6.17. It is an error to create this variant with a major number >=§ 10;
25    /// see [`checked`][`Self::checked`] for a way to guard against this.
26    Pre10mm(u32, u32, u32),
27    /// PostgreSQL 10+, with major version number, e.g. 10. It is an error to
28    /// create this variant with a major number < 10; see
29    /// [`checked`][`Self::checked`] for a way to guard against this.
30    Post10m(u32),
31    /// PostgreSQL 10+, with major and minor version number, e.g. 10.3. It is an
32    /// error to create this variant with a major number < 10; see
33    /// [`checked`][`Self::checked`] for a way to guard against this.
34    Post10mm(u32, u32),
35}
36
37/// Convert a [`PartialVersion`] into a [`Version`] that's useful for
38/// comparisons.
39///
40/// The [`Version`] returned has 0 (zero) in the place of the missing parts. For
41/// example, a partial version of `9.6.*` becomes `9.6.0`, and `12.*` becomes
42/// `12.0`.
43impl From<&PartialVersion> for Version {
44    fn from(partial: &PartialVersion) -> Self {
45        use PartialVersion::*;
46        match *partial {
47            Pre10m(a, b) => Version::Pre10(a, b, 0),
48            Pre10mm(a, b, c) => Version::Pre10(a, b, c),
49            Post10m(a) => Version::Post10(a, 0),
50            Post10mm(a, b) => Version::Post10(a, b),
51        }
52    }
53}
54
55/// See `From<&PartialVersion> for Version`.
56impl From<PartialVersion> for Version {
57    fn from(partial: PartialVersion) -> Self {
58        (&partial).into()
59    }
60}
61
62/// Convert a [`Version`] into a [`PartialVersion`].
63impl From<&Version> for PartialVersion {
64    fn from(version: &Version) -> Self {
65        use Version::*;
66        match *version {
67            Pre10(a, b, c) => PartialVersion::Pre10mm(a, b, c),
68            Post10(a, b) => PartialVersion::Post10mm(a, b),
69        }
70    }
71}
72
73/// See `From<&Version> for PartialVersion`.
74impl From<Version> for PartialVersion {
75    fn from(version: Version) -> Self {
76        (&version).into()
77    }
78}
79
80impl PartialVersion {
81    /// Return self if it is a valid [`PartialVersion`].
82    ///
83    /// This can be necessary when a [`PartialVersion`] has been constructed
84    /// directly. It checks that [`PartialVersion::Pre10m`] and
85    /// [`PartialVersion::Pre10mm`] have a major version number less than 10,
86    /// and that [`PartialVersion::Post10m`] and [`PartialVersion::Post10mm`]
87    /// have a major version number greater than or equal to 10.
88    pub fn checked(self) -> Result<Self, VersionError> {
89        use PartialVersion::*;
90        match self {
91            Pre10m(a, ..) | Pre10mm(a, ..) if a < 10 => Ok(self),
92            Post10m(a) | Post10mm(a, ..) if a >= 10 => Ok(self),
93            _ => Err(VersionError::BadlyFormed { text: Some(self.to_string()) }),
94        }
95    }
96
97    /// Is the given [`Version`] compatible with this [`PartialVersion`]?
98    ///
99    /// Put another way: can a server of the given [`Version`] be used to run a
100    /// cluster of this [`PartialVersion`]?
101    ///
102    /// This is an interesting question to answer because clusters contain a
103    /// file named `PG_VERSION` which containing just the major version number
104    /// of the cluster's files, e.g. "15", or the major.point number for older
105    /// PostgreSQL releases, e.g. "9.6".
106    ///
107    /// For versions of PostgreSQL before 10, this means that the given
108    /// version's major and point numbers must match exactly, and the minor
109    /// number must be greater than or equal to this `PartialVersion`'s minor
110    /// number. When this `PartialVersion` has no point or minor number, the
111    /// given version is assumed to be compatible.
112    ///
113    /// For versions of PostgreSQL after and including 10, this means that the
114    /// given version's major number must match exactly, and the minor number
115    /// must be greater than or equal to this `PartialVersion`'s minor number.
116    /// When this `PartialVersion` has no minor number, the given version is
117    /// assumed to be compatible.
118    #[allow(dead_code)]
119    pub fn compatible(&self, version: Version) -> bool {
120        use PartialVersion::*;
121        match (*self, version) {
122            (Pre10m(a, b), Version::Pre10(x, y, _)) => a == x && b == y,
123            (Pre10mm(a, b, c), Version::Pre10(x, y, z)) => a == x && b == y && c <= z,
124            (Post10m(a), Version::Post10(x, _)) => a == x,
125            (Post10mm(a, b), Version::Post10(x, y)) => a == x && b <= y,
126            _ => false,
127        }
128    }
129
130    /// Remove minor number.
131    #[must_use]
132    pub fn widened(&self) -> PartialVersion {
133        use PartialVersion::*;
134        match self {
135            Pre10mm(a, b, _) => Pre10m(*a, *b),
136            Post10mm(a, _) => Post10m(*a),
137            _ => *self,
138        }
139    }
140
141    /// Provide a sort key that implements [`Ord`].
142    ///
143    /// `PartialVersion` does not implement [`Eq`] or [`Ord`] because they would
144    /// disagree with its [`PartialEq`] and [`PartialOrd`] implementations, so
145    /// this function provides a sort key that implements [`Ord`] and can be
146    /// used with sorting functions, e.g. [`slice::sort_by_key`].
147    #[allow(dead_code)]
148    pub fn sort_key(&self) -> (u32, Option<u32>, Option<u32>) {
149        use PartialVersion::*;
150        match *self {
151            Pre10m(a, b) => (a, Some(b), None),
152            Pre10mm(a, b, c) => (a, Some(b), Some(c)),
153            Post10m(a) => (a, None, None),
154            Post10mm(a, b) => (a, Some(b), None),
155        }
156    }
157}
158
159impl PartialEq for PartialVersion {
160    fn eq(&self, other: &Self) -> bool {
161        self.partial_cmp(other) == Some(Ordering::Equal)
162    }
163}
164
165impl PartialOrd for PartialVersion {
166    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
167        use PartialVersion::*;
168        match (*self, *other) {
169            (Pre10m(a, b), Pre10m(x, y)) => Some((a, b).cmp(&(x, y))),
170            (Pre10m(a, b), Pre10mm(x, y, _)) => Some((a, b).cmp(&(x, y))),
171            (Pre10mm(a, b, _), Pre10m(x, y)) => Some((a, b).cmp(&(x, y))),
172            (Pre10mm(a, b, c), Pre10mm(x, y, z)) => Some((a, b, c).cmp(&(x, y, z))),
173
174            (Post10m(a), Post10m(x)) => Some(a.cmp(&x)),
175            (Post10m(a), Post10mm(x, _)) => Some(a.cmp(&x)),
176            (Post10mm(a, _), Post10m(x)) => Some(a.cmp(&x)),
177            (Post10mm(a, b), Post10mm(x, y)) => Some((a, b).cmp(&(x, y))),
178
179            (Pre10m(..), Post10m(..)) => Some(Ordering::Less),
180            (Pre10m(..), Post10mm(..)) => Some(Ordering::Less),
181            (Pre10mm(..), Post10m(..)) => Some(Ordering::Less),
182            (Pre10mm(..), Post10mm(..)) => Some(Ordering::Less),
183
184            (Post10m(..), Pre10m(..)) => Some(Ordering::Greater),
185            (Post10m(..), Pre10mm(..)) => Some(Ordering::Greater),
186            (Post10mm(..), Pre10m(..)) => Some(Ordering::Greater),
187            (Post10mm(..), Pre10mm(..)) => Some(Ordering::Greater),
188        }
189    }
190}
191
192impl fmt::Display for PartialVersion {
193    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
194        match *self {
195            Self::Pre10m(a, b) => fmt.pad(&format!("{a}.{b}")),
196            Self::Pre10mm(a, b, c) => fmt.pad(&format!("{a}.{b}.{c}")),
197            Self::Post10m(a) => fmt.pad(&format!("{a}")),
198            Self::Post10mm(a, b) => fmt.pad(&format!("{a}.{b}")),
199        }
200    }
201}
202
203impl FromStr for PartialVersion {
204    type Err = VersionError;
205
206    fn from_str(s: &str) -> Result<Self, Self::Err> {
207        static RE: LazyLock<Regex> = LazyLock::new(|| {
208            Regex::new(r"(?x) \b (\d+) (?: [.] (\d+) (?: [.] (\d+) )? )? \b")
209                .expect("invalid regex (for matching partial PostgreSQL versions)")
210        });
211        match RE.captures(s) {
212            Some(caps) => match (
213                caps.get(1).and_then(|n| n.as_str().parse::<u32>().ok()),
214                caps.get(2).and_then(|n| n.as_str().parse::<u32>().ok()),
215                caps.get(3).and_then(|n| n.as_str().parse::<u32>().ok()),
216            ) {
217                (Some(a), Some(b), None) if a < 10 => Ok(Self::Pre10m(a, b)),
218                (Some(a), Some(b), Some(c)) if a < 10 => Ok(Self::Pre10mm(a, b, c)),
219                (Some(a), None, None) if a >= 10 => Ok(Self::Post10m(a)),
220                (Some(a), Some(b), None) if a >= 10 => Ok(Self::Post10mm(a, b)),
221                _ => Err(VersionError::BadlyFormed { text: Some(s.into()) }),
222            },
223            None => Err(VersionError::NotFound { text: Some(s.into()) }),
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::super::{Version, VersionError::*};
231    use super::{PartialVersion, PartialVersion::*};
232
233    use rand::rng;
234    use rand::seq::SliceRandom;
235
236    #[test]
237    fn parses_version_below_10() {
238        assert_eq!(Ok(Pre10mm(9, 6, 17)), "9.6.17".parse());
239        assert_eq!(Ok(Pre10m(9, 6)), "9.6".parse());
240    }
241
242    #[test]
243    fn parses_version_above_10() {
244        assert_eq!(Ok(Post10mm(12, 2)), "12.2".parse());
245        assert_eq!(Ok(Post10m(12)), "12".parse());
246    }
247
248    #[test]
249    fn parse_returns_error_when_version_is_invalid() {
250        // 4294967295 is (2^32 + 1), so won't fit in a u32.
251        assert!(matches!(
252            "4294967296.0".parse::<PartialVersion>(),
253            Err(BadlyFormed { .. })
254        ));
255        // Before version 10, there are always at least two parts in a version.
256        assert!(matches!(
257            "9".parse::<PartialVersion>(),
258            Err(BadlyFormed { .. })
259        ));
260        // From version 10 onwards, there are only two parts in a version.
261        assert!(matches!(
262            "10.10.10".parse::<PartialVersion>(),
263            Err(BadlyFormed { .. })
264        ));
265    }
266
267    #[test]
268    fn parse_returns_error_when_version_not_found() {
269        assert!(matches!(
270            "foo".parse::<PartialVersion>(),
271            Err(NotFound { .. })
272        ));
273    }
274
275    #[test]
276    fn checked_returns_self_when_variant_is_valid() {
277        use PartialVersion::*;
278        assert_eq!(Ok(Pre10m(9, 0)), Pre10m(9, 0).checked());
279        assert_eq!(Ok(Pre10mm(9, 0, 0)), Pre10mm(9, 0, 0).checked());
280        assert_eq!(Ok(Post10m(10)), Post10m(10).checked());
281        assert_eq!(Ok(Post10mm(10, 0)), Post10mm(10, 0).checked());
282    }
283
284    #[test]
285    fn checked_returns_error_when_variant_is_invalid() {
286        use PartialVersion::*;
287        assert!(matches!(Pre10m(10, 0).checked(), Err(BadlyFormed { .. })));
288        assert!(matches!(
289            Pre10mm(10, 0, 0).checked(),
290            Err(BadlyFormed { .. })
291        ));
292        assert!(matches!(Post10m(9).checked(), Err(BadlyFormed { .. })));
293        assert!(matches!(Post10mm(9, 0).checked(), Err(BadlyFormed { .. })));
294    }
295
296    #[test]
297    fn displays_version_below_10() {
298        assert_eq!("9.6.17", format!("{}", Pre10mm(9, 6, 17)));
299        assert_eq!("9.6", format!("{}", Pre10m(9, 6)));
300    }
301
302    #[test]
303    fn displays_version_above_10() {
304        assert_eq!("12.2", format!("{}", Post10mm(12, 2)));
305        assert_eq!("12", format!("{}", Post10m(12)));
306    }
307
308    #[test]
309    fn converts_partial_version_to_version() {
310        assert_eq!(Version::Pre10(9, 1, 2), Pre10mm(9, 1, 2).into());
311        assert_eq!(Version::Pre10(9, 1, 0), Pre10m(9, 1).into());
312        assert_eq!(Version::Post10(14, 2), Post10mm(14, 2).into());
313        assert_eq!(Version::Post10(14, 0), Post10m(14).into());
314    }
315
316    #[test]
317    fn compatible_below_10() {
318        let version = "9.6.16".parse().unwrap();
319        assert!(Pre10mm(9, 6, 16).compatible(version));
320        assert!(Pre10m(9, 6).compatible(version));
321    }
322
323    #[test]
324    fn not_compatible_below_10() {
325        let version = "9.6.16".parse().unwrap();
326        assert!(!Pre10mm(9, 6, 17).compatible(version));
327        assert!(!Pre10m(9, 7).compatible(version));
328        assert!(!Pre10mm(8, 6, 16).compatible(version));
329        assert!(!Pre10m(8, 6).compatible(version));
330    }
331
332    #[test]
333    fn compatible_above_10() {
334        let version = "12.6".parse().unwrap();
335        assert!(Post10mm(12, 6).compatible(version));
336        assert!(Post10m(12).compatible(version));
337    }
338
339    #[test]
340    fn not_compatible_above_10() {
341        let version = "12.6".parse().unwrap();
342        assert!(!Post10mm(12, 7).compatible(version));
343        assert!(!Post10m(13).compatible(version));
344        assert!(!Post10mm(11, 6).compatible(version));
345        assert!(!Post10m(11).compatible(version));
346    }
347
348    #[test]
349    fn not_compatible_below_10_with_above_10() {
350        let version = "12.6".parse().unwrap();
351        assert!(!Pre10m(9, 1).compatible(version));
352        assert!(!Pre10mm(9, 1, 2).compatible(version));
353        let version = "9.1.2".parse().unwrap();
354        assert!(!Post10m(12).compatible(version));
355        assert!(!Post10mm(12, 6).compatible(version));
356    }
357
358    #[test]
359    fn widened_removes_minor_or_patch_number() {
360        assert_eq!(Pre10mm(9, 1, 2), Pre10m(9, 1));
361        assert_eq!(Post10mm(12, 9), Post10m(12));
362        assert_eq!(Pre10m(9, 1), Pre10m(9, 1));
363        assert_eq!(Post10m(12), Post10m(12));
364    }
365
366    #[test]
367    fn partial_ord_works_as_expected() {
368        let mut versions = vec![
369            Pre10mm(9, 10, 11),
370            Pre10mm(9, 10, 12),
371            Pre10m(8, 11),
372            Pre10m(9, 11),
373            Pre10m(9, 12),
374            Post10mm(10, 11),
375            Post10m(11),
376        ];
377        let mut rng = rng();
378        for _ in 0..1000 {
379            versions.shuffle(&mut rng);
380            versions.sort_by(|a, b| a.partial_cmp(b).unwrap());
381            assert_eq!(
382                versions,
383                vec![
384                    Pre10m(8, 11),
385                    Pre10mm(9, 10, 11),
386                    Pre10mm(9, 10, 12),
387                    Pre10m(9, 11),
388                    Pre10m(9, 12),
389                    Post10mm(10, 11),
390                    Post10m(11),
391                ]
392            );
393        }
394    }
395
396    #[test]
397    fn sort_key_works_as_expected() {
398        let mut versions = vec![
399            Pre10mm(9, 0, 0),
400            Pre10mm(9, 10, 11),
401            Pre10mm(9, 10, 12),
402            Pre10m(9, 0),
403            Pre10m(8, 11),
404            Pre10m(9, 11),
405            Pre10m(9, 12),
406            Post10mm(10, 11),
407            Post10m(11),
408        ];
409        let mut rng = rng();
410        for _ in 0..1000 {
411            versions.shuffle(&mut rng);
412            versions.sort_by_key(PartialVersion::sort_key);
413            assert_eq!(
414                versions,
415                vec![
416                    Pre10m(8, 11),
417                    Pre10m(9, 0),
418                    Pre10mm(9, 0, 0),
419                    Pre10mm(9, 10, 11),
420                    Pre10mm(9, 10, 12),
421                    Pre10m(9, 11),
422                    Pre10m(9, 12),
423                    Post10mm(10, 11),
424                    Post10m(11),
425                ]
426            );
427        }
428    }
429}