pgdo/version/
current.rs

1//! Parse PostgreSQL version numbers.
2//!
3//! ```rust
4//! # use pgdo::version::Version;
5//! assert_eq!(Ok(Version::Pre10(9, 6, 17)), "9.6.17".parse());
6//! assert_eq!(Ok(Version::Post10(14, 6)), "14.6".parse());
7//! ```
8//!
9//! See the [PostgreSQL "Versioning Policy" page][versioning] for information on
10//! PostgreSQL's versioning scheme.
11//!
12//! [versioning]: https://www.postgresql.org/support/versioning/
13
14// TODO: Parse `server_version_num`/`PG_VERSION_NUM`, e.g. 120007 for version
15// 12.7, 90624 for 9.6.24. See https://pgpedia.info/s/server_version_num.html
16// and https://www.postgresql.org/docs/16/runtime-config-preset.html.
17
18use std::fmt;
19use std::str::FromStr;
20
21use regex::Regex;
22
23use super::VersionError;
24
25/// Represents a full PostgreSQL version. This is the kind of thing we see when
26/// running `pg_ctl --version` for example.
27///
28/// The "Current minor" column shown on the [PostgreSQL "Versioning Policy"
29/// page][versioning] is what this models.
30///
31/// [versioning]: https://www.postgresql.org/support/versioning/
32#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub enum Version {
34    /// Pre-PostgreSQL 10, with major, point, and minor version numbers, e.g.
35    /// 9.6.17. It is an error to create this variant with a major number >= 10.
36    Pre10(u32, u32, u32),
37    /// PostgreSQL 10+, with major and minor version number, e.g. 10.3. It is an
38    /// error to create this variant with a major number < 10.
39    Post10(u32, u32),
40}
41
42impl fmt::Display for Version {
43    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
44        match self {
45            Version::Pre10(a, b, c) => fmt.pad(&format!("{a}.{b}.{c}")),
46            Version::Post10(a, b) => fmt.pad(&format!("{a}.{b}")),
47        }
48    }
49}
50
51impl FromStr for Version {
52    type Err = VersionError;
53
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        lazy_static! {
56            static ref RE: Regex = Regex::new(r"(?x) \b (\d+) [.] (\d+) (?: [.] (\d+) )? \b")
57                .expect("invalid regex (for matching PostgreSQL versions)");
58        }
59        let badly_formed = |_| VersionError::BadlyFormed { text: Some(s.into()) };
60        match RE.captures(s) {
61            Some(caps) => {
62                let a = caps[1].parse::<u32>().map_err(badly_formed)?;
63                let b = caps[2].parse::<u32>().map_err(badly_formed)?;
64                match caps.get(3) {
65                    None if a >= 10 => Ok(Version::Post10(a, b)),
66                    None => Err(VersionError::BadlyFormed { text: Some(s.into()) }),
67                    Some(_) if a >= 10 => Err(VersionError::BadlyFormed { text: Some(s.into()) }),
68                    Some(m) => Ok(m
69                        .as_str()
70                        .parse::<u32>()
71                        .map(|c| Version::Pre10(a, b, c))
72                        .map_err(badly_formed)?),
73                }
74            }
75            None => Err(VersionError::NotFound { text: Some(s.into()) }),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::Version::{Post10, Pre10};
83    use super::{Version, VersionError::*};
84
85    use std::cmp::Ordering;
86
87    #[test]
88    fn parses_version_below_10() {
89        assert_eq!(Ok(Pre10(9, 6, 17)), "9.6.17".parse());
90    }
91
92    #[test]
93    fn parses_version_above_10() {
94        assert_eq!(Ok(Post10(12, 2)), "12.2".parse());
95    }
96
97    #[test]
98    fn parse_returns_error_when_version_is_invalid() {
99        // 4294967295 is (2^32 + 1), so won't fit in a u32.
100        assert!(matches!(
101            "4294967296.0".parse::<Version>(),
102            Err(BadlyFormed { .. })
103        ));
104    }
105
106    #[test]
107    fn parse_returns_error_when_version_not_found() {
108        assert!(matches!("foo".parse::<Version>(), Err(NotFound { .. })));
109    }
110
111    #[test]
112    fn displays_version_below_10() {
113        assert_eq!("9.6.17", format!("{}", Pre10(9, 6, 17)));
114    }
115
116    #[test]
117    fn displays_version_above_10() {
118        assert_eq!("12.2", format!("{}", Post10(12, 2)));
119    }
120
121    #[test]
122        #[rustfmt::skip]
123        fn derive_partial_ord_works_as_expected() {
124            assert_eq!(Pre10(9, 10, 11).partial_cmp(&Post10(10, 11)), Some(Ordering::Less));
125            assert_eq!(Post10(10, 11).partial_cmp(&Pre10(9, 10, 11)), Some(Ordering::Greater));
126            assert_eq!(Pre10(9, 10, 11).partial_cmp(&Pre10(9, 10, 11)), Some(Ordering::Equal));
127            assert_eq!(Post10(10, 11).partial_cmp(&Post10(10, 11)), Some(Ordering::Equal));
128        }
129
130    #[test]
131    fn derive_ord_works_as_expected() {
132        let mut versions = vec![
133            Pre10(9, 10, 11),
134            Post10(10, 11),
135            Post10(14, 2),
136            Pre10(9, 10, 12),
137            Post10(10, 12),
138        ];
139        versions.sort(); // Uses `Ord`.
140        assert_eq!(
141            versions,
142            vec![
143                Pre10(9, 10, 11),
144                Pre10(9, 10, 12),
145                Post10(10, 11),
146                Post10(10, 12),
147                Post10(14, 2)
148            ]
149        );
150    }
151}