pgdo/version/
partial.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::str::FromStr;
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        lazy_static! {
208            static ref RE: Regex =
209                Regex::new(r"(?x) \b (\d+) (?: [.] (\d+) (?: [.] (\d+) )? )? \b")
210                    .expect("invalid regex (for matching partial PostgreSQL versions)");
211        }
212        match RE.captures(s) {
213            Some(caps) => match (
214                caps.get(1).and_then(|n| n.as_str().parse::<u32>().ok()),
215                caps.get(2).and_then(|n| n.as_str().parse::<u32>().ok()),
216                caps.get(3).and_then(|n| n.as_str().parse::<u32>().ok()),
217            ) {
218                (Some(a), Some(b), None) if a < 10 => Ok(Self::Pre10m(a, b)),
219                (Some(a), Some(b), Some(c)) if a < 10 => Ok(Self::Pre10mm(a, b, c)),
220                (Some(a), None, None) if a >= 10 => Ok(Self::Post10m(a)),
221                (Some(a), Some(b), None) if a >= 10 => Ok(Self::Post10mm(a, b)),
222                _ => Err(VersionError::BadlyFormed { text: Some(s.into()) }),
223            },
224            None => Err(VersionError::NotFound { text: Some(s.into()) }),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::super::{Version, VersionError::*};
232    use super::{PartialVersion, PartialVersion::*};
233
234    use rand::seq::SliceRandom;
235    use rand::thread_rng;
236
237    #[test]
238    fn parses_version_below_10() {
239        assert_eq!(Ok(Pre10mm(9, 6, 17)), "9.6.17".parse());
240        assert_eq!(Ok(Pre10m(9, 6)), "9.6".parse());
241    }
242
243    #[test]
244    fn parses_version_above_10() {
245        assert_eq!(Ok(Post10mm(12, 2)), "12.2".parse());
246        assert_eq!(Ok(Post10m(12)), "12".parse());
247    }
248
249    #[test]
250    fn parse_returns_error_when_version_is_invalid() {
251        // 4294967295 is (2^32 + 1), so won't fit in a u32.
252        assert!(matches!(
253            "4294967296.0".parse::<PartialVersion>(),
254            Err(BadlyFormed { .. })
255        ));
256        // Before version 10, there are always at least two parts in a version.
257        assert!(matches!(
258            "9".parse::<PartialVersion>(),
259            Err(BadlyFormed { .. })
260        ));
261        // From version 10 onwards, there are only two parts in a version.
262        assert!(matches!(
263            "10.10.10".parse::<PartialVersion>(),
264            Err(BadlyFormed { .. })
265        ));
266    }
267
268    #[test]
269    fn parse_returns_error_when_version_not_found() {
270        assert!(matches!(
271            "foo".parse::<PartialVersion>(),
272            Err(NotFound { .. })
273        ));
274    }
275
276    #[test]
277    fn checked_returns_self_when_variant_is_valid() {
278        use PartialVersion::*;
279        assert_eq!(Ok(Pre10m(9, 0)), Pre10m(9, 0).checked());
280        assert_eq!(Ok(Pre10mm(9, 0, 0)), Pre10mm(9, 0, 0).checked());
281        assert_eq!(Ok(Post10m(10)), Post10m(10).checked());
282        assert_eq!(Ok(Post10mm(10, 0)), Post10mm(10, 0).checked());
283    }
284
285    #[test]
286    fn checked_returns_error_when_variant_is_invalid() {
287        use PartialVersion::*;
288        assert!(matches!(Pre10m(10, 0).checked(), Err(BadlyFormed { .. })));
289        assert!(matches!(
290            Pre10mm(10, 0, 0).checked(),
291            Err(BadlyFormed { .. })
292        ));
293        assert!(matches!(Post10m(9).checked(), Err(BadlyFormed { .. })));
294        assert!(matches!(Post10mm(9, 0).checked(), Err(BadlyFormed { .. })));
295    }
296
297    #[test]
298    fn displays_version_below_10() {
299        assert_eq!("9.6.17", format!("{}", Pre10mm(9, 6, 17)));
300        assert_eq!("9.6", format!("{}", Pre10m(9, 6)));
301    }
302
303    #[test]
304    fn displays_version_above_10() {
305        assert_eq!("12.2", format!("{}", Post10mm(12, 2)));
306        assert_eq!("12", format!("{}", Post10m(12)));
307    }
308
309    #[test]
310    fn converts_partial_version_to_version() {
311        assert_eq!(Version::Pre10(9, 1, 2), Pre10mm(9, 1, 2).into());
312        assert_eq!(Version::Pre10(9, 1, 0), Pre10m(9, 1).into());
313        assert_eq!(Version::Post10(14, 2), Post10mm(14, 2).into());
314        assert_eq!(Version::Post10(14, 0), Post10m(14).into());
315    }
316
317    #[test]
318    fn compatible_below_10() {
319        let version = "9.6.16".parse().unwrap();
320        assert!(Pre10mm(9, 6, 16).compatible(version));
321        assert!(Pre10m(9, 6).compatible(version));
322    }
323
324    #[test]
325    fn not_compatible_below_10() {
326        let version = "9.6.16".parse().unwrap();
327        assert!(!Pre10mm(9, 6, 17).compatible(version));
328        assert!(!Pre10m(9, 7).compatible(version));
329        assert!(!Pre10mm(8, 6, 16).compatible(version));
330        assert!(!Pre10m(8, 6).compatible(version));
331    }
332
333    #[test]
334    fn compatible_above_10() {
335        let version = "12.6".parse().unwrap();
336        assert!(Post10mm(12, 6).compatible(version));
337        assert!(Post10m(12).compatible(version));
338    }
339
340    #[test]
341    fn not_compatible_above_10() {
342        let version = "12.6".parse().unwrap();
343        assert!(!Post10mm(12, 7).compatible(version));
344        assert!(!Post10m(13).compatible(version));
345        assert!(!Post10mm(11, 6).compatible(version));
346        assert!(!Post10m(11).compatible(version));
347    }
348
349    #[test]
350    fn not_compatible_below_10_with_above_10() {
351        let version = "12.6".parse().unwrap();
352        assert!(!Pre10m(9, 1).compatible(version));
353        assert!(!Pre10mm(9, 1, 2).compatible(version));
354        let version = "9.1.2".parse().unwrap();
355        assert!(!Post10m(12).compatible(version));
356        assert!(!Post10mm(12, 6).compatible(version));
357    }
358
359    #[test]
360    fn widened_removes_minor_or_patch_number() {
361        assert_eq!(Pre10mm(9, 1, 2), Pre10m(9, 1));
362        assert_eq!(Post10mm(12, 9), Post10m(12));
363        assert_eq!(Pre10m(9, 1), Pre10m(9, 1));
364        assert_eq!(Post10m(12), Post10m(12));
365    }
366
367    #[test]
368    fn partial_ord_works_as_expected() {
369        let mut versions = vec![
370            Pre10mm(9, 10, 11),
371            Pre10mm(9, 10, 12),
372            Pre10m(8, 11),
373            Pre10m(9, 11),
374            Pre10m(9, 12),
375            Post10mm(10, 11),
376            Post10m(11),
377        ];
378        let mut rng = thread_rng();
379        for _ in 0..1000 {
380            versions.shuffle(&mut rng);
381            versions.sort_by(|a, b| a.partial_cmp(b).unwrap());
382            assert_eq!(
383                versions,
384                vec![
385                    Pre10m(8, 11),
386                    Pre10mm(9, 10, 11),
387                    Pre10mm(9, 10, 12),
388                    Pre10m(9, 11),
389                    Pre10m(9, 12),
390                    Post10mm(10, 11),
391                    Post10m(11),
392                ]
393            );
394        }
395    }
396
397    #[test]
398    fn sort_key_works_as_expected() {
399        let mut versions = vec![
400            Pre10mm(9, 0, 0),
401            Pre10mm(9, 10, 11),
402            Pre10mm(9, 10, 12),
403            Pre10m(9, 0),
404            Pre10m(8, 11),
405            Pre10m(9, 11),
406            Pre10m(9, 12),
407            Post10mm(10, 11),
408            Post10m(11),
409        ];
410        let mut rng = thread_rng();
411        for _ in 0..1000 {
412            versions.shuffle(&mut rng);
413            versions.sort_by_key(PartialVersion::sort_key);
414            assert_eq!(
415                versions,
416                vec![
417                    Pre10m(8, 11),
418                    Pre10m(9, 0),
419                    Pre10mm(9, 0, 0),
420                    Pre10mm(9, 10, 11),
421                    Pre10mm(9, 10, 12),
422                    Pre10m(9, 11),
423                    Pre10m(9, 12),
424                    Post10mm(10, 11),
425                    Post10m(11),
426                ]
427            );
428        }
429    }
430}