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