pgrx_pg_config/
lib.rs

1//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2//LICENSE
3//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4//LICENSE
5//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6//LICENSE
7//LICENSE All rights reserved.
8//LICENSE
9//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10//! Wrapper around Postgres' `pg_config` command-line tool
11use eyre::{eyre, WrapErr};
12use owo_colors::OwoColorize;
13use serde::{Deserialize, Serialize};
14use std::collections::{BTreeMap, HashMap};
15use std::env::consts::EXE_SUFFIX;
16use std::ffi::OsString;
17use std::fmt::{self, Debug, Display, Formatter};
18use std::io::ErrorKind;
19use std::path::PathBuf;
20use std::process::{Command, Stdio};
21use std::str::FromStr;
22use thiserror::Error;
23use url::Url;
24
25mod decoding;
26
27pub mod cargo;
28
29pub static BASE_POSTGRES_PORT_NO: u16 = 28800;
30pub static BASE_POSTGRES_TESTING_PORT_NO: u16 = 32200;
31
32/// The flags to specify to get a "C.UTF-8" locale on this system, or "C" locale on systems without
33/// a "C.UTF-8" locale equivalent.
34pub fn get_c_locale_flags() -> &'static [&'static str] {
35    #[cfg(all(target_family = "unix", not(target_os = "macos")))]
36    {
37        match Command::new("locale").arg("-a").output() {
38            Ok(cmd)
39                if String::from_utf8_lossy(&cmd.stdout)
40                    .lines()
41                    .any(|l| l == "C.UTF-8" || l == "C.utf8") =>
42            {
43                &["--locale=C.UTF-8"]
44            }
45            // fallback to C if we can't list locales or don't have C.UTF-8
46            _ => &["--locale=C"],
47        }
48    }
49    #[cfg(target_os = "macos")]
50    {
51        &["--locale=C", "--lc-ctype=UTF-8"]
52    }
53    #[cfg(target_os = "windows")]
54    {
55        &["--locale=C"]
56    }
57}
58
59// These methods were originally in `pgrx-utils`, but in an effort to consolidate
60// dependencies, the decision was made to package them into wherever made the
61// most sense. In this case, it made the most sense to put them into this
62// pgrx-pg-config crate. That doesn't mean they can't be moved at a later date.
63mod path_methods;
64pub use path_methods::{get_target_dir, prefix_path};
65
66use crate::decoding::decode_from_bytes;
67
68#[derive(Copy, Clone, Debug, Eq, PartialEq)]
69pub enum PgMinorVersion {
70    Latest,
71    Release(u16),
72    Beta(u16),
73    Rc(u16),
74}
75
76impl Display for PgMinorVersion {
77    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78        match self {
79            PgMinorVersion::Latest => write!(f, ".LATEST"),
80            PgMinorVersion::Release(v) => write!(f, ".{v}"),
81            PgMinorVersion::Beta(v) => write!(f, "beta{v}"),
82            PgMinorVersion::Rc(v) => write!(f, "rc{v}"),
83        }
84    }
85}
86
87impl PgMinorVersion {
88    fn version(&self) -> Option<u16> {
89        match self {
90            PgMinorVersion::Latest => None,
91            PgMinorVersion::Release(v) | PgMinorVersion::Beta(v) | PgMinorVersion::Rc(v) => {
92                Some(*v)
93            }
94        }
95    }
96}
97
98#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct PgVersion {
100    pub major: u16,
101    pub minor: PgMinorVersion,
102    pub url: Option<Url>,
103}
104
105impl PgVersion {
106    pub const fn new(major: u16, minor: PgMinorVersion, url: Option<Url>) -> PgVersion {
107        PgVersion { major, minor, url }
108    }
109
110    pub fn minor(&self) -> Option<u16> {
111        self.minor.version()
112    }
113}
114
115impl Display for PgVersion {
116    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117        write!(f, "{}{}", self.major, self.minor)
118    }
119}
120
121#[derive(Clone, Debug)]
122pub struct PgConfig {
123    version: Option<PgVersion>,
124    pg_config: Option<PathBuf>,
125    known_props: Option<BTreeMap<String, String>>,
126    base_port: u16,
127    base_testing_port: u16,
128}
129
130impl Display for PgConfig {
131    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
132        write!(f, "{}", self.version().expect("failed to create version string"))
133    }
134}
135
136impl Default for PgConfig {
137    fn default() -> Self {
138        PgConfig {
139            version: None,
140            pg_config: None,
141            known_props: None,
142            base_port: BASE_POSTGRES_PORT_NO,
143            base_testing_port: BASE_POSTGRES_TESTING_PORT_NO,
144        }
145    }
146}
147
148impl From<PgVersion> for PgConfig {
149    fn from(version: PgVersion) -> Self {
150        PgConfig { version: Some(version), pg_config: None, ..Default::default() }
151    }
152}
153
154impl PgConfig {
155    pub fn new(pg_config: PathBuf, base_port: u16, base_testing_port: u16) -> Self {
156        PgConfig {
157            version: None,
158            pg_config: Some(pg_config),
159            known_props: None,
160            base_port,
161            base_testing_port,
162        }
163    }
164
165    pub fn new_with_defaults(pg_config: PathBuf) -> Self {
166        PgConfig {
167            version: None,
168            pg_config: Some(pg_config),
169            known_props: None,
170            base_port: BASE_POSTGRES_PORT_NO,
171            base_testing_port: BASE_POSTGRES_TESTING_PORT_NO,
172        }
173    }
174
175    pub fn from_path() -> Self {
176        let path =
177            pathsearch::find_executable_in_path("pg_config").unwrap_or_else(|| "pg_config".into());
178        Self::new_with_defaults(path)
179    }
180
181    /// Construct a new [`PgConfig`] from the set of environment variables that are prefixed with
182    /// `PGRX_PG_CONFIG_`.
183    ///
184    /// It also requires that the `PGRX_PG_CONFIG_AS_ENV` variable be set to some value that isn't
185    /// the string `"false"`.
186    pub fn from_env() -> eyre::Result<Self> {
187        if !Self::is_in_environment() {
188            Err(eyre::eyre!("`PgConfig` not described in the environment"))
189        } else {
190            const PREFIX: &str = "PGRX_PG_CONFIG_";
191
192            let mut known_props = BTreeMap::new();
193            for (k, v) in std::env::vars().filter(|(k, _)| k.starts_with(PREFIX)) {
194                // reformat the key to look like an argument option to `pg_config`
195                let prop = format!("--{}", k.trim_start_matches(PREFIX).to_lowercase());
196                known_props.insert(prop, v);
197            }
198
199            Ok(Self {
200                version: None,
201                pg_config: None,
202                known_props: Some(known_props),
203                base_port: 0,
204                base_testing_port: 0,
205            })
206        }
207    }
208
209    pub fn is_in_environment() -> bool {
210        match std::env::var("PGRX_PG_CONFIG_AS_ENV") {
211            Ok(value) => value == "true",
212            _ => false,
213        }
214    }
215
216    pub fn is_real(&self) -> bool {
217        self.pg_config.is_some()
218    }
219
220    pub fn label(&self) -> eyre::Result<String> {
221        Ok(format!("pg{}", self.major_version()?))
222    }
223
224    pub fn path(&self) -> Option<PathBuf> {
225        self.pg_config.clone()
226    }
227
228    pub fn parent_path(&self) -> PathBuf {
229        self.path().unwrap().parent().unwrap().to_path_buf()
230    }
231
232    fn parse_version_str(version_str: &str) -> eyre::Result<(u16, PgMinorVersion)> {
233        let version_parts = version_str.split_whitespace().collect::<Vec<&str>>();
234        let mut version = version_parts
235            .get(1)
236            .ok_or_else(|| eyre!("invalid version string: {version_str}"))?
237            .split('.')
238            .collect::<Vec<&str>>();
239
240        let mut beta = false;
241        let mut rc = false;
242
243        if version.len() == 1 {
244            // it's hopefully a "beta" or "rc" release
245            let first = &version[0];
246
247            if first.contains("beta") {
248                beta = true;
249                version = first.split("beta").collect();
250            } else if first.contains("rc") {
251                rc = true;
252                version = first.split("rc").collect();
253            } else {
254                return Err(eyre!("invalid version string: {version_str}"));
255            }
256        }
257
258        let major = u16::from_str(version[0])
259            .map_err(|e| eyre!("invalid major version number `{}`: {:?}", version[0], e))?;
260        let mut minor = version[1];
261        let mut end_index = minor.len();
262        for (i, c) in minor.chars().enumerate() {
263            if !c.is_ascii_digit() {
264                end_index = i;
265                break;
266            }
267        }
268        minor = &minor[0..end_index];
269        let minor = u16::from_str(minor)
270            .map_err(|e| eyre!("invalid minor version number `{minor}`: {e:?}"))?;
271        let minor = if beta {
272            PgMinorVersion::Beta(minor)
273        } else if rc {
274            PgMinorVersion::Rc(minor)
275        } else {
276            PgMinorVersion::Release(minor)
277        };
278        Ok((major, minor))
279    }
280
281    pub fn get_version(&self) -> eyre::Result<PgVersion> {
282        let version_string = self.run("--version")?;
283        let (major, minor) = Self::parse_version_str(&version_string)?;
284        Ok(PgVersion::new(major, minor, None))
285    }
286
287    pub fn major_version(&self) -> eyre::Result<u16> {
288        match &self.version {
289            Some(version) => Ok(version.major),
290            None => Ok(self.get_version()?.major),
291        }
292    }
293
294    fn minor_version(&self) -> eyre::Result<PgMinorVersion> {
295        match &self.version {
296            Some(version) => Ok(version.minor),
297            None => Ok(self.get_version()?.minor),
298        }
299    }
300
301    pub fn version(&self) -> eyre::Result<String> {
302        match self.version.as_ref() {
303            Some(pgver) => Ok(pgver.to_string()),
304            None => {
305                let major = self.major_version()?;
306                let minor = self.minor_version()?;
307                let version = format!("{major}{minor}");
308                Ok(version)
309            }
310        }
311    }
312
313    pub fn url(&self) -> Option<&Url> {
314        match &self.version {
315            Some(version) => version.url.as_ref(),
316            None => None,
317        }
318    }
319
320    pub fn port(&self) -> eyre::Result<u16> {
321        Ok(self.base_port + self.major_version()?)
322    }
323
324    pub fn test_port(&self) -> eyre::Result<u16> {
325        Ok(self.base_testing_port + self.major_version()?)
326    }
327
328    pub fn host(&self) -> &'static str {
329        "localhost"
330    }
331
332    pub fn bin_dir(&self) -> eyre::Result<PathBuf> {
333        Ok(self.run("--bindir")?.into())
334    }
335
336    pub fn lib_dir(&self) -> eyre::Result<PathBuf> {
337        Ok(self.run("--libdir")?.into())
338    }
339
340    pub fn postmaster_path(&self) -> eyre::Result<PathBuf> {
341        let mut path = self.bin_dir()?;
342        path.push(format!("postgres{EXE_SUFFIX}"));
343        Ok(path)
344    }
345
346    pub fn initdb_path(&self) -> eyre::Result<PathBuf> {
347        let mut path = self.bin_dir()?;
348        path.push(format!("initdb{EXE_SUFFIX}"));
349        Ok(path)
350    }
351
352    pub fn createdb_path(&self) -> eyre::Result<PathBuf> {
353        let mut path = self.bin_dir()?;
354        path.push(format!("createdb{EXE_SUFFIX}"));
355        Ok(path)
356    }
357
358    pub fn dropdb_path(&self) -> eyre::Result<PathBuf> {
359        let mut path = self.bin_dir()?;
360        path.push(format!("dropdb{EXE_SUFFIX}"));
361        Ok(path)
362    }
363
364    pub fn pg_ctl_path(&self) -> eyre::Result<PathBuf> {
365        let mut path = self.bin_dir()?;
366        path.push(format!("pg_ctl{EXE_SUFFIX}"));
367        Ok(path)
368    }
369
370    pub fn psql_path(&self) -> eyre::Result<PathBuf> {
371        let mut path = self.bin_dir()?;
372        path.push(format!("psql{EXE_SUFFIX}"));
373        Ok(path)
374    }
375
376    pub fn pg_regress_path(&self) -> eyre::Result<PathBuf> {
377        let mut pgxs_path = self.pgxs_path()?;
378        pgxs_path.pop(); // pop the `pgxs.mk` file at the end
379        pgxs_path.pop(); // pop the `makefiles` directory in which it lives
380        let mut pgregress_path = pgxs_path;
381        pgregress_path.push("test");
382        pgregress_path.push("regress");
383        pgregress_path.push("pg_regress");
384        Ok(pgregress_path)
385    }
386
387    pub fn pgxs_path(&self) -> eyre::Result<PathBuf> {
388        self.run("--pgxs").map(PathBuf::from)
389    }
390
391    pub fn data_dir(&self) -> eyre::Result<PathBuf> {
392        let mut path = Pgrx::home()?;
393        path.push(format!("data-{}", self.major_version()?));
394        Ok(path)
395    }
396
397    pub fn log_file(&self) -> eyre::Result<PathBuf> {
398        let mut path = Pgrx::home()?;
399        path.push(format!("{}.log", self.major_version()?));
400        Ok(path)
401    }
402
403    /// a vaguely-parsed "--configure"
404    pub fn configure(&self) -> eyre::Result<BTreeMap<String, String>> {
405        let stdout = self.run("--configure")?;
406        Ok(stdout
407            .split('\'')
408            .filter(|s| s != &"" && s != &" ")
409            .map(|entry| match entry.split_once('=') {
410                Some((k, v)) => (k.to_owned(), v.to_owned()),
411                // some keys are about mere presence
412                None => (entry.to_owned(), String::from("")),
413            })
414            .collect())
415    }
416
417    pub fn pkgincludedir(&self) -> eyre::Result<PathBuf> {
418        Ok(self.run("--pkgincludedir")?.into())
419    }
420
421    pub fn includedir_server(&self) -> eyre::Result<PathBuf> {
422        Ok(self.run("--includedir-server")?.into())
423    }
424
425    pub fn includedir_server_port_win32(&self) -> eyre::Result<PathBuf> {
426        let includedir_server = self.includedir_server()?;
427        Ok(includedir_server.join("port").join("win32"))
428    }
429
430    pub fn includedir_server_port_win32_msvc(&self) -> eyre::Result<PathBuf> {
431        let includedir_server = self.includedir_server()?;
432        Ok(includedir_server.join("port").join("win32_msvc"))
433    }
434
435    pub fn pkglibdir(&self) -> eyre::Result<PathBuf> {
436        Ok(self.run("--pkglibdir")?.into())
437    }
438
439    pub fn sharedir(&self) -> eyre::Result<PathBuf> {
440        Ok(self.run("--sharedir")?.into())
441    }
442
443    pub fn cppflags(&self) -> eyre::Result<OsString> {
444        Ok(self.run("--cppflags")?.into())
445    }
446
447    pub fn extension_dir(&self) -> eyre::Result<PathBuf> {
448        let mut path = self.sharedir()?;
449        path.push("extension");
450        Ok(path)
451    }
452
453    fn run(&self, arg: &str) -> eyre::Result<String> {
454        if self.known_props.is_some() {
455            // we have some known properties, so use them.  We'll return an `ErrorKind::InvalidData`
456            // if the caller asks for a property we don't have
457            Ok(self
458                .known_props
459                .as_ref()
460                .unwrap()
461                .get(arg)
462                .ok_or_else(|| {
463                    std::io::Error::new(
464                        ErrorKind::InvalidData,
465                        format!("`PgConfig` has no known property named {arg}"),
466                    )
467                })
468                .cloned()?)
469        } else {
470            // we don't have any known properties, so fall through to asking the `pg_config`
471            // that's either in the environment or on the PATH
472            let pg_config = self.pg_config.clone().unwrap_or_else(|| {
473                std::env::var("PG_CONFIG").unwrap_or_else(|_| "pg_config".to_string()).into()
474            });
475
476            match Command::new(&pg_config).arg(arg).output() {
477                Ok(output) => Ok(decode_from_bytes(&output.stdout).trim().to_string()),
478                Err(e) => match e.kind() {
479                    ErrorKind::NotFound => Err(e).wrap_err_with(|| {
480                        let pg_config_str = pg_config.display().to_string();
481
482                        if pg_config_str == "pg_config" {
483                            format!("Unable to find `{}` on the system $PATH", "pg_config".yellow())
484                        } else if pg_config_str.starts_with('~') {
485                            format!("The specified pg_config binary, {}, does not exist. The shell didn't expand the `~`", pg_config_str.yellow())
486                        } else {
487                            format!(
488                                "The specified pg_config binary, `{}`, does not exist",
489                                pg_config_str.yellow()
490                            )
491                        }
492                    }),
493                    _ => Err(e.into()),
494                },
495            }
496        }
497    }
498}
499
500#[derive(Debug)]
501pub struct Pgrx {
502    pg_configs: Vec<PgConfig>,
503    base_port: u16,
504    base_testing_port: u16,
505}
506
507impl Default for Pgrx {
508    fn default() -> Self {
509        Self {
510            pg_configs: vec![],
511            base_port: BASE_POSTGRES_PORT_NO,
512            base_testing_port: BASE_POSTGRES_TESTING_PORT_NO,
513        }
514    }
515}
516
517#[derive(Debug, Default, Serialize, Deserialize)]
518pub struct ConfigToml {
519    pub configs: HashMap<String, PathBuf>,
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub base_port: Option<u16>,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub base_testing_port: Option<u16>,
524}
525
526pub enum PgConfigSelector<'a> {
527    All,
528    Specific(&'a str),
529    Environment,
530}
531
532impl<'a> PgConfigSelector<'a> {
533    pub fn new(label: &'a str) -> Self {
534        if label == "all" {
535            PgConfigSelector::All
536        } else {
537            PgConfigSelector::Specific(label)
538        }
539    }
540}
541
542#[derive(Debug, Error)]
543pub enum PgrxHomeError {
544    #[error("You don't seem to have a home directory")]
545    NoHomeDirectory,
546    // allow caller to decide whether it is safe to enumerate paths
547    #[error("$PGRX_HOME does not exist")]
548    MissingPgrxHome(PathBuf),
549    #[error(transparent)]
550    IoError(#[from] std::io::Error),
551}
552
553impl From<PgrxHomeError> for std::io::Error {
554    fn from(value: PgrxHomeError) -> Self {
555        match value {
556            PgrxHomeError::NoHomeDirectory => {
557                std::io::Error::new(ErrorKind::NotFound, value.to_string())
558            }
559            PgrxHomeError::MissingPgrxHome(_) => {
560                std::io::Error::new(ErrorKind::NotFound, value.to_string())
561            }
562            PgrxHomeError::IoError(e) => e,
563        }
564    }
565}
566
567impl Pgrx {
568    pub fn new(base_port: u16, base_testing_port: u16) -> Self {
569        Pgrx { pg_configs: vec![], base_port, base_testing_port }
570    }
571
572    pub fn from_config() -> eyre::Result<Self> {
573        match std::env::var("PGRX_PG_CONFIG_PATH") {
574            Ok(pg_config) => {
575                // we have an environment variable that tells us the pg_config to use
576                let mut pgrx = Pgrx::default();
577                pgrx.push(PgConfig::new(pg_config.into(), pgrx.base_port, pgrx.base_testing_port));
578                Ok(pgrx)
579            }
580            Err(_) => {
581                // we'll get what we need from cargo-pgrx' config.toml file
582                let path = Pgrx::config_toml()?;
583                if !path.try_exists()? {
584                    return Err(eyre!(
585                        "{} not found.  Have you run `{}` yet?",
586                        path.display(),
587                        "cargo pgrx init".bold().yellow()
588                    ));
589                };
590
591                match toml::from_str::<ConfigToml>(&std::fs::read_to_string(&path)?) {
592                    Ok(configs) => {
593                        let mut pgrx = Pgrx::new(
594                            configs.base_port.unwrap_or(BASE_POSTGRES_PORT_NO),
595                            configs.base_testing_port.unwrap_or(BASE_POSTGRES_TESTING_PORT_NO),
596                        );
597
598                        for (_, v) in configs.configs {
599                            pgrx.push(PgConfig::new(v, pgrx.base_port, pgrx.base_testing_port));
600                        }
601                        Ok(pgrx)
602                    }
603                    Err(e) => {
604                        Err(e).wrap_err_with(|| format!("Could not read `{}`", path.display()))
605                    }
606                }
607            }
608        }
609    }
610
611    pub fn push(&mut self, pg_config: PgConfig) {
612        self.pg_configs.push(pg_config);
613    }
614
615    /// Returns an iterator of all "configured" `PgConfig`s we know about.
616    ///
617    /// If the `which` argument is [`PgConfigSelector::All`] **and** the environment variable
618    /// `PGRX_PG_CONFIG_AS_ENV` is set to a value that isn't `"false"`then this function will return
619    /// a one-element iterator that represents that single "pg_config".
620    ///
621    /// Otherwise, we'll follow the rules of [`PgConfigSelector::All`] being everything in `$PGRX_HOME/config.toml`,
622    /// [`PgConfigSelector::Specific`] being that specific version from `$PGRX_HOME/config.toml`, and
623    /// [`PgConfigSelector::Environment`] being the one described in the environment.
624    pub fn iter(
625        &self,
626        which: PgConfigSelector,
627    ) -> impl std::iter::Iterator<Item = eyre::Result<PgConfig>> {
628        match (which, PgConfig::is_in_environment()) {
629            (PgConfigSelector::All, true) | (PgConfigSelector::Environment, _) => {
630                vec![PgConfig::from_env()].into_iter()
631            }
632
633            (PgConfigSelector::All, _) => {
634                let mut configs = self.pg_configs.iter().collect::<Vec<_>>();
635                configs.sort_by(|a, b| {
636                    a.major_version()
637                        .expect("no major version")
638                        .cmp(&b.major_version().expect("no major version"))
639                });
640
641                configs.into_iter().map(|c| Ok(c.clone())).collect::<Vec<_>>().into_iter()
642            }
643            (PgConfigSelector::Specific(label), _) => vec![self.get(label)].into_iter(),
644        }
645    }
646
647    pub fn get(&self, label: &str) -> eyre::Result<PgConfig> {
648        for pg_config in self.pg_configs.iter() {
649            if pg_config.label()? == label {
650                return Ok(pg_config.clone());
651            }
652        }
653        Err(eyre!("Postgres `{label}` is not managed by pgrx"))
654    }
655
656    /// Returns true if the specified `label` represents a Postgres version number feature flag,
657    /// such as `pg14` or `pg15`
658    pub fn is_feature_flag(&self, label: &str) -> bool {
659        for pgver in SUPPORTED_VERSIONS() {
660            if label == format!("pg{}", pgver.major) {
661                return true;
662            }
663        }
664        false
665    }
666
667    pub fn home() -> Result<PathBuf, PgrxHomeError> {
668        let pgrx_home = std::env::var("PGRX_HOME").map_or_else(
669            |_| {
670                let mut pgrx_home = match home::home_dir() {
671                    Some(home) => home,
672                    None => return Err(PgrxHomeError::NoHomeDirectory),
673                };
674
675                pgrx_home.push(".pgrx");
676                Ok(pgrx_home)
677            },
678            |v| Ok(v.into()),
679        )?;
680
681        match pgrx_home.try_exists() {
682            Ok(true) => Ok(pgrx_home),
683            Ok(false) => Err(PgrxHomeError::MissingPgrxHome(pgrx_home)),
684            Err(e) => Err(PgrxHomeError::IoError(e)),
685        }
686    }
687
688    /// Get the postmaster stub directory
689    ///
690    /// We isolate postmaster stubs to an independent directory instead of alongside the postmaster
691    /// because in the case of `cargo pgrx install` the `pg_config` may not necessarily be one managed
692    /// by pgrx.
693    pub fn postmaster_stub_dir() -> Result<PathBuf, std::io::Error> {
694        let mut stub_dir = Self::home()?;
695        stub_dir.push("postmaster_stubs");
696        Ok(stub_dir)
697    }
698
699    pub fn config_toml() -> Result<PathBuf, std::io::Error> {
700        let mut path = Pgrx::home()?;
701        path.push("config.toml");
702        Ok(path)
703    }
704}
705
706#[allow(non_snake_case)]
707pub fn SUPPORTED_VERSIONS() -> Vec<PgVersion> {
708    vec![
709        PgVersion::new(13, PgMinorVersion::Latest, None),
710        PgVersion::new(14, PgMinorVersion::Latest, None),
711        PgVersion::new(15, PgMinorVersion::Latest, None),
712        PgVersion::new(16, PgMinorVersion::Latest, None),
713        PgVersion::new(17, PgMinorVersion::Latest, None),
714        PgVersion::new(
715            18,
716            PgMinorVersion::Beta(1),
717            Some(
718                Url::parse(
719                    "https://ftp.postgresql.org/pub/source/v18beta1/postgresql-18beta1.tar.bz2",
720                )
721                .expect("malformed pg18beta1 url"),
722            ),
723        ),
724    ]
725}
726
727pub fn is_supported_major_version(v: u16) -> bool {
728    SUPPORTED_VERSIONS().into_iter().any(|pgver| pgver.major == v)
729}
730
731pub fn createdb(
732    pg_config: &PgConfig,
733    dbname: &str,
734    is_test: bool,
735    if_not_exists: bool,
736    runas: Option<String>,
737) -> eyre::Result<bool> {
738    if if_not_exists && does_db_exist(pg_config, dbname)? {
739        return Ok(false);
740    }
741
742    println!("{} database {}", "    Creating".bold().green(), dbname.bold().cyan());
743    let createdb_path = pg_config.createdb_path()?;
744    let mut command = if let Some(runas) = runas {
745        let mut cmd = Command::new("sudo");
746        cmd.arg("-u").arg(runas).arg(createdb_path);
747        cmd
748    } else {
749        Command::new(createdb_path)
750    };
751    command
752        .env_remove("PGDATABASE")
753        .env_remove("PGHOST")
754        .env_remove("PGPORT")
755        .env_remove("PGUSER")
756        .arg("-h")
757        .arg(pg_config.host())
758        .arg("-p")
759        .arg(if is_test {
760            pg_config.test_port()?.to_string()
761        } else {
762            pg_config.port()?.to_string()
763        })
764        .arg(dbname)
765        .stdout(Stdio::piped())
766        .stderr(Stdio::piped());
767
768    let command_str = format!("{command:?}");
769
770    let child = command.spawn().wrap_err_with(|| {
771        format!("Failed to spawn process for creating database using command: '{command_str}': ")
772    })?;
773
774    let output = child.wait_with_output().wrap_err_with(|| {
775        format!(
776            "failed waiting for spawned process to create database using command: '{command_str}': "
777        )
778    })?;
779
780    if !output.status.success() {
781        return Err(eyre!(
782            "problem running createdb: {}\n\n{}{}",
783            command_str,
784            decode_from_bytes(&output.stdout),
785            decode_from_bytes(&output.stderr)
786        ));
787    }
788
789    Ok(true)
790}
791
792pub fn dropdb(
793    pg_config: &PgConfig,
794    dbname: &str,
795    is_test: bool,
796    runas: Option<String>,
797) -> eyre::Result<bool> {
798    if !does_db_exist(pg_config, dbname)? {
799        return Ok(false);
800    }
801
802    println!("{} database {}", "    Dropping".bold().green(), dbname.bold().cyan());
803    let createdb_path = pg_config.dropdb_path()?;
804    let mut command = if let Some(runas) = runas {
805        let mut cmd = Command::new("sudo");
806        cmd.arg("-u").arg(runas).arg(createdb_path);
807        cmd
808    } else {
809        Command::new(createdb_path)
810    };
811    command
812        .env_remove("PGDATABASE")
813        .env_remove("PGHOST")
814        .env_remove("PGPORT")
815        .env_remove("PGUSER")
816        .arg("-h")
817        .arg(pg_config.host())
818        .arg("-p")
819        .arg(if is_test {
820            pg_config.test_port()?.to_string()
821        } else {
822            pg_config.port()?.to_string()
823        })
824        .arg(dbname)
825        .stdout(Stdio::piped())
826        .stderr(Stdio::piped());
827
828    let command_str = format!("{command:?}");
829
830    let child = command.spawn().wrap_err_with(|| {
831        format!("Failed to spawn process for dropping database using command: '{command_str}': ")
832    })?;
833
834    let output = child.wait_with_output().wrap_err_with(|| {
835        format!(
836            "failed waiting for spawned process to drop database using command: '{command_str}': "
837        )
838    })?;
839
840    if !output.status.success() {
841        return Err(eyre!(
842            "problem running dropdb: {}\n\n{}{}",
843            command_str,
844            decode_from_bytes(&output.stdout),
845            decode_from_bytes(&output.stderr)
846        ));
847    }
848
849    Ok(true)
850}
851
852fn does_db_exist(pg_config: &PgConfig, dbname: &str) -> eyre::Result<bool> {
853    let mut command = Command::new(pg_config.psql_path()?);
854    command
855        .arg("-XqAt")
856        .env_remove("PGUSER")
857        .arg("-h")
858        .arg(pg_config.host())
859        .arg("-p")
860        .arg(pg_config.port()?.to_string())
861        .arg("-c")
862        .arg(format!(
863            "select count(*) from pg_database where datname = '{}';",
864            dbname.replace('\'', "''")
865        ))
866        .arg("template1")
867        .stdout(Stdio::piped())
868        .stderr(Stdio::piped());
869
870    let command_str = format!("{command:?}");
871    let output = command.output()?;
872
873    if !output.status.success() {
874        Err(eyre!(
875            "problem checking if database '{}' exists: {}\n\n{}{}",
876            dbname,
877            command_str,
878            decode_from_bytes(&output.stdout),
879            decode_from_bytes(&output.stderr)
880        ))
881    } else {
882        let count = i32::from_str(decode_from_bytes(&output.stdout).trim())
883            .wrap_err("result is not a number")?;
884        Ok(count > 0)
885    }
886}
887
888#[test]
889fn parse_version() {
890    // Check some valid version strings
891    let versions = [
892        ("PostgreSQL 10.22", 10, 22),
893        ("PostgreSQL 11.2", 11, 2),
894        ("PostgreSQL 11.17", 11, 17),
895        ("PostgreSQL 12.12", 12, 12),
896        ("PostgreSQL 13.8", 13, 8),
897        ("PostgreSQL 14.5", 14, 5),
898        ("PostgreSQL 11.2-FOO-BAR+", 11, 2),
899        ("PostgreSQL 10.22-", 10, 22),
900    ];
901    for (s, major_expected, minor_expected) in versions {
902        let (major, minor) =
903            PgConfig::parse_version_str(s).expect("Unable to parse version string");
904        assert_eq!(major, major_expected, "Major version should match");
905        assert_eq!(minor.version(), Some(minor_expected), "Minor version should match");
906    }
907
908    // Check some invalid version strings
909    let _ = PgConfig::parse_version_str("10.22").expect_err("Parsed invalid version string");
910    let _ =
911        PgConfig::parse_version_str("PostgresSQL 10").expect_err("Parsed invalid version string");
912    let _ =
913        PgConfig::parse_version_str("PostgresSQL 10.").expect_err("Parsed invalid version string");
914    let _ =
915        PgConfig::parse_version_str("PostgresSQL 12.f").expect_err("Parsed invalid version string");
916    let _ =
917        PgConfig::parse_version_str("PostgresSQL .53").expect_err("Parsed invalid version string");
918}
919
920#[test]
921fn from_empty_env() -> eyre::Result<()> {
922    // without "PGRX_PG_CONFIG_AS_ENV" we can't get one of these
923    let pg_config = PgConfig::from_env();
924    assert!(pg_config.is_err());
925
926    // but now we can
927    std::env::set_var("PGRX_PG_CONFIG_AS_ENV", "true");
928    std::env::set_var("PGRX_PG_CONFIG_VERSION", "PostgresSQL 15.1");
929    std::env::set_var("PGRX_PG_CONFIG_INCLUDEDIR-SERVER", "/path/to/server/headers");
930    std::env::set_var("PGRX_PG_CONFIG_CPPFLAGS", "some cpp flags");
931
932    let pg_config = PgConfig::from_env().unwrap();
933    assert_eq!(pg_config.major_version()?, 15, "Major version should match");
934    assert_eq!(
935        pg_config.minor_version()?,
936        PgMinorVersion::Release(1),
937        "Minor version should match"
938    );
939    assert_eq!(
940        pg_config.includedir_server()?,
941        PathBuf::from("/path/to/server/headers"),
942        "includdir_server should match"
943    );
944    assert_eq!(pg_config.cppflags()?, OsString::from("some cpp flags"), "cppflags should match");
945
946    // we didn't set this one in our environment
947    assert!(pg_config.sharedir().is_err());
948    Ok(())
949}