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