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 lib_dir(&self) -> eyre::Result<PathBuf> {
328        Ok(self.run("--libdir")?.into())
329    }
330
331    pub fn postmaster_path(&self) -> eyre::Result<PathBuf> {
332        let mut path = self.bin_dir()?;
333        path.push("postgres");
334
335        Ok(path)
336    }
337
338    pub fn initdb_path(&self) -> eyre::Result<PathBuf> {
339        let mut path = self.bin_dir()?;
340        path.push("initdb");
341        Ok(path)
342    }
343
344    pub fn createdb_path(&self) -> eyre::Result<PathBuf> {
345        let mut path = self.bin_dir()?;
346        path.push("createdb");
347        Ok(path)
348    }
349
350    pub fn dropdb_path(&self) -> eyre::Result<PathBuf> {
351        let mut path = self.bin_dir()?;
352        path.push("dropdb");
353        Ok(path)
354    }
355
356    pub fn psql_path(&self) -> eyre::Result<PathBuf> {
357        let mut path = self.bin_dir()?;
358        path.push("psql");
359        Ok(path)
360    }
361
362    pub fn data_dir(&self) -> eyre::Result<PathBuf> {
363        let mut path = Pgrx::home()?;
364        path.push(format!("data-{}", self.major_version()?));
365        Ok(path)
366    }
367
368    pub fn log_file(&self) -> eyre::Result<PathBuf> {
369        let mut path = Pgrx::home()?;
370        path.push(format!("{}.log", self.major_version()?));
371        Ok(path)
372    }
373
374    /// a vaguely-parsed "--configure"
375    pub fn configure(&self) -> eyre::Result<BTreeMap<String, String>> {
376        let stdout = self.run("--configure")?;
377        Ok(stdout
378            .split('\'')
379            .filter(|s| s != &"" && s != &" ")
380            .map(|entry| match entry.split_once('=') {
381                Some((k, v)) => (k.to_owned(), v.to_owned()),
382                // some keys are about mere presence
383                None => (entry.to_owned(), String::from("")),
384            })
385            .collect())
386    }
387
388    pub fn includedir_server(&self) -> eyre::Result<PathBuf> {
389        Ok(self.run("--includedir-server")?.into())
390    }
391
392    pub fn pkglibdir(&self) -> eyre::Result<PathBuf> {
393        Ok(self.run("--pkglibdir")?.into())
394    }
395
396    pub fn sharedir(&self) -> eyre::Result<PathBuf> {
397        Ok(self.run("--sharedir")?.into())
398    }
399
400    pub fn cppflags(&self) -> eyre::Result<OsString> {
401        Ok(self.run("--cppflags")?.into())
402    }
403
404    pub fn extension_dir(&self) -> eyre::Result<PathBuf> {
405        let mut path = self.sharedir()?;
406        path.push("extension");
407        Ok(path)
408    }
409
410    fn run(&self, arg: &str) -> eyre::Result<String> {
411        if self.known_props.is_some() {
412            // we have some known properties, so use them.  We'll return an `ErrorKind::InvalidData`
413            // if the caller asks for a property we don't have
414            Ok(self
415                .known_props
416                .as_ref()
417                .unwrap()
418                .get(arg)
419                .ok_or_else(|| {
420                    std::io::Error::new(
421                        ErrorKind::InvalidData,
422                        format!("`PgConfig` has no known property named {arg}"),
423                    )
424                })
425                .cloned()?)
426        } else {
427            // we don't have any known properties, so fall through to asking the `pg_config`
428            // that's either in the environment or on the PATH
429            let pg_config = self.pg_config.clone().unwrap_or_else(|| {
430                std::env::var("PG_CONFIG").unwrap_or_else(|_| "pg_config".to_string()).into()
431            });
432
433            match Command::new(&pg_config).arg(arg).output() {
434                Ok(output) => Ok(String::from_utf8(output.stdout).unwrap().trim().to_string()),
435                Err(e) => match e.kind() {
436                    ErrorKind::NotFound => Err(e).wrap_err_with(|| {
437                        let pg_config_str = pg_config.display().to_string();
438
439                        if pg_config_str == "pg_config" {
440                            format!("Unable to find `{}` on the system $PATH", "pg_config".yellow())
441                        } else if pg_config_str.starts_with('~') {
442                            format!("The specified pg_config binary, {}, does not exist. The shell didn't expand the `~`", pg_config_str.yellow())
443                        } else {
444                            format!(
445                                "The specified pg_config binary, `{}`, does not exist",
446                                pg_config_str.yellow()
447                            )
448                        }
449                    }),
450                    _ => Err(e.into()),
451                },
452            }
453        }
454    }
455}
456
457#[derive(Debug)]
458pub struct Pgrx {
459    pg_configs: Vec<PgConfig>,
460    base_port: u16,
461    base_testing_port: u16,
462}
463
464impl Default for Pgrx {
465    fn default() -> Self {
466        Self {
467            pg_configs: vec![],
468            base_port: BASE_POSTGRES_PORT_NO,
469            base_testing_port: BASE_POSTGRES_TESTING_PORT_NO,
470        }
471    }
472}
473
474#[derive(Debug, Default, Serialize, Deserialize)]
475pub struct ConfigToml {
476    pub configs: HashMap<String, PathBuf>,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub base_port: Option<u16>,
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub base_testing_port: Option<u16>,
481}
482
483pub enum PgConfigSelector<'a> {
484    All,
485    Specific(&'a str),
486    Environment,
487}
488
489impl<'a> PgConfigSelector<'a> {
490    pub fn new(label: &'a str) -> Self {
491        if label == "all" {
492            PgConfigSelector::All
493        } else {
494            PgConfigSelector::Specific(label)
495        }
496    }
497}
498
499#[derive(Debug, Error)]
500pub enum PgrxHomeError {
501    #[error("You don't seem to have a home directory")]
502    NoHomeDirectory,
503    // allow caller to decide whether it is safe to enumerate paths
504    #[error("$PGRX_HOME does not exist")]
505    MissingPgrxHome(PathBuf),
506    #[error(transparent)]
507    IoError(#[from] std::io::Error),
508}
509
510impl From<PgrxHomeError> for std::io::Error {
511    fn from(value: PgrxHomeError) -> Self {
512        match value {
513            PgrxHomeError::NoHomeDirectory => {
514                std::io::Error::new(ErrorKind::NotFound, value.to_string())
515            }
516            PgrxHomeError::MissingPgrxHome(_) => {
517                std::io::Error::new(ErrorKind::NotFound, value.to_string())
518            }
519            PgrxHomeError::IoError(e) => e,
520        }
521    }
522}
523
524impl Pgrx {
525    pub fn new(base_port: u16, base_testing_port: u16) -> Self {
526        Pgrx { pg_configs: vec![], base_port, base_testing_port }
527    }
528
529    pub fn from_config() -> eyre::Result<Self> {
530        match std::env::var("PGRX_PG_CONFIG_PATH") {
531            Ok(pg_config) => {
532                // we have an environment variable that tells us the pg_config to use
533                let mut pgrx = Pgrx::default();
534                pgrx.push(PgConfig::new(pg_config.into(), pgrx.base_port, pgrx.base_testing_port));
535                Ok(pgrx)
536            }
537            Err(_) => {
538                // we'll get what we need from cargo-pgrx' config.toml file
539                let path = Pgrx::config_toml()?;
540                if !path.try_exists()? {
541                    return Err(eyre!(
542                        "{} not found.  Have you run `{}` yet?",
543                        path.display(),
544                        "cargo pgrx init".bold().yellow()
545                    ));
546                };
547
548                match toml::from_str::<ConfigToml>(&std::fs::read_to_string(&path)?) {
549                    Ok(configs) => {
550                        let mut pgrx = Pgrx::new(
551                            configs.base_port.unwrap_or(BASE_POSTGRES_PORT_NO),
552                            configs.base_testing_port.unwrap_or(BASE_POSTGRES_TESTING_PORT_NO),
553                        );
554
555                        for (_, v) in configs.configs {
556                            pgrx.push(PgConfig::new(v, pgrx.base_port, pgrx.base_testing_port));
557                        }
558                        Ok(pgrx)
559                    }
560                    Err(e) => {
561                        Err(e).wrap_err_with(|| format!("Could not read `{}`", path.display()))
562                    }
563                }
564            }
565        }
566    }
567
568    pub fn push(&mut self, pg_config: PgConfig) {
569        self.pg_configs.push(pg_config);
570    }
571
572    /// Returns an iterator of all "configured" `PgConfig`s we know about.
573    ///
574    /// If the `which` argument is [`PgConfigSelector::All`] **and** the environment variable
575    /// `PGRX_PG_CONFIG_AS_ENV` is set to a value that isn't `"false"`then this function will return
576    /// a one-element iterator that represents that single "pg_config".
577    ///
578    /// Otherwise, we'll follow the rules of [`PgConfigSelector::All`] being everything in `$PGRX_HOME/config.toml`,
579    /// [`PgConfigSelector::Specific`] being that specific version from `$PGRX_HOME/config.toml`, and
580    /// [`PgConfigSelector::Environment`] being the one described in the environment.
581    pub fn iter(
582        &self,
583        which: PgConfigSelector,
584    ) -> impl std::iter::Iterator<Item = eyre::Result<PgConfig>> {
585        match (which, PgConfig::is_in_environment()) {
586            (PgConfigSelector::All, true) | (PgConfigSelector::Environment, _) => {
587                vec![PgConfig::from_env()].into_iter()
588            }
589
590            (PgConfigSelector::All, _) => {
591                let mut configs = self.pg_configs.iter().collect::<Vec<_>>();
592                configs.sort_by(|a, b| {
593                    a.major_version()
594                        .expect("no major version")
595                        .cmp(&b.major_version().expect("no major version"))
596                });
597
598                configs.into_iter().map(|c| Ok(c.clone())).collect::<Vec<_>>().into_iter()
599            }
600            (PgConfigSelector::Specific(label), _) => vec![self.get(label)].into_iter(),
601        }
602    }
603
604    pub fn get(&self, label: &str) -> eyre::Result<PgConfig> {
605        for pg_config in self.pg_configs.iter() {
606            if pg_config.label()? == label {
607                return Ok(pg_config.clone());
608            }
609        }
610        Err(eyre!("Postgres `{label}` is not managed by pgrx"))
611    }
612
613    /// Returns true if the specified `label` represents a Postgres version number feature flag,
614    /// such as `pg14` or `pg15`
615    pub fn is_feature_flag(&self, label: &str) -> bool {
616        for pgver in SUPPORTED_VERSIONS() {
617            if label == format!("pg{}", pgver.major) {
618                return true;
619            }
620        }
621        false
622    }
623
624    pub fn home() -> Result<PathBuf, PgrxHomeError> {
625        let pgrx_home = std::env::var("PGRX_HOME").map_or_else(
626            |_| {
627                let mut pgrx_home = match home::home_dir() {
628                    Some(home) => home,
629                    None => return Err(PgrxHomeError::NoHomeDirectory),
630                };
631
632                pgrx_home.push(".pgrx");
633                Ok(pgrx_home)
634            },
635            |v| Ok(v.into()),
636        )?;
637
638        match pgrx_home.try_exists() {
639            Ok(true) => Ok(pgrx_home),
640            Ok(false) => Err(PgrxHomeError::MissingPgrxHome(pgrx_home)),
641            Err(e) => Err(PgrxHomeError::IoError(e)),
642        }
643    }
644
645    /// Get the postmaster stub directory
646    ///
647    /// We isolate postmaster stubs to an independent directory instead of alongside the postmaster
648    /// because in the case of `cargo pgrx install` the `pg_config` may not necessarily be one managed
649    /// by pgrx.
650    pub fn postmaster_stub_dir() -> Result<PathBuf, std::io::Error> {
651        let mut stub_dir = Self::home()?;
652        stub_dir.push("postmaster_stubs");
653        Ok(stub_dir)
654    }
655
656    pub fn config_toml() -> Result<PathBuf, std::io::Error> {
657        let mut path = Pgrx::home()?;
658        path.push("config.toml");
659        Ok(path)
660    }
661}
662
663#[allow(non_snake_case)]
664pub fn SUPPORTED_VERSIONS() -> Vec<PgVersion> {
665    vec![
666        PgVersion::new(12, PgMinorVersion::Latest, None),
667        PgVersion::new(13, PgMinorVersion::Latest, None),
668        PgVersion::new(14, PgMinorVersion::Latest, None),
669        PgVersion::new(15, PgMinorVersion::Latest, None),
670        PgVersion::new(16, PgMinorVersion::Latest, None),
671        PgVersion::new(17, PgMinorVersion::Latest, None),
672    ]
673}
674
675pub fn is_supported_major_version(v: u16) -> bool {
676    SUPPORTED_VERSIONS().into_iter().any(|pgver| pgver.major == v)
677}
678
679pub fn createdb(
680    pg_config: &PgConfig,
681    dbname: &str,
682    is_test: bool,
683    if_not_exists: bool,
684    runas: Option<String>,
685) -> eyre::Result<bool> {
686    if if_not_exists && does_db_exist(pg_config, dbname)? {
687        return Ok(false);
688    }
689
690    println!("{} database {}", "     Creating".bold().green(), dbname);
691    let createdb_path = pg_config.createdb_path()?;
692    let mut command = if let Some(runas) = runas {
693        let mut cmd = Command::new("sudo");
694        cmd.arg("-u").arg(runas).arg(createdb_path);
695        cmd
696    } else {
697        Command::new(createdb_path)
698    };
699    command
700        .env_remove("PGDATABASE")
701        .env_remove("PGHOST")
702        .env_remove("PGPORT")
703        .env_remove("PGUSER")
704        .arg("-h")
705        .arg(pg_config.host())
706        .arg("-p")
707        .arg(if is_test {
708            pg_config.test_port()?.to_string()
709        } else {
710            pg_config.port()?.to_string()
711        })
712        .arg(dbname)
713        .stdout(Stdio::piped())
714        .stderr(Stdio::piped());
715
716    let command_str = format!("{command:?}");
717
718    let child = command.spawn().wrap_err_with(|| {
719        format!("Failed to spawn process for creating database using command: '{command_str}': ")
720    })?;
721
722    let output = child.wait_with_output().wrap_err_with(|| {
723        format!(
724            "failed waiting for spawned process to create database using command: '{command_str}': "
725        )
726    })?;
727
728    if !output.status.success() {
729        return Err(eyre!(
730            "problem running createdb: {}\n\n{}{}",
731            command_str,
732            String::from_utf8(output.stdout).unwrap(),
733            String::from_utf8(output.stderr).unwrap()
734        ));
735    }
736
737    Ok(true)
738}
739
740fn does_db_exist(pg_config: &PgConfig, dbname: &str) -> eyre::Result<bool> {
741    let mut command = Command::new(pg_config.psql_path()?);
742    command
743        .arg("-XqAt")
744        .env_remove("PGUSER")
745        .arg("-h")
746        .arg(pg_config.host())
747        .arg("-p")
748        .arg(pg_config.port()?.to_string())
749        .arg("template1")
750        .arg("-c")
751        .arg(format!(
752            "select count(*) from pg_database where datname = '{}';",
753            dbname.replace('\'', "''")
754        ))
755        .stdout(Stdio::piped())
756        .stderr(Stdio::piped());
757
758    let command_str = format!("{command:?}");
759    let output = command.output()?;
760
761    if !output.status.success() {
762        Err(eyre!(
763            "problem checking if database '{}' exists: {}\n\n{}{}",
764            dbname,
765            command_str,
766            String::from_utf8(output.stdout).unwrap(),
767            String::from_utf8(output.stderr).unwrap()
768        ))
769    } else {
770        let count = i32::from_str(String::from_utf8(output.stdout).unwrap().trim())
771            .wrap_err("result is not a number")?;
772        Ok(count > 0)
773    }
774}
775
776#[test]
777fn parse_version() {
778    // Check some valid version strings
779    let versions = [
780        ("PostgreSQL 10.22", 10, 22),
781        ("PostgreSQL 11.2", 11, 2),
782        ("PostgreSQL 11.17", 11, 17),
783        ("PostgreSQL 12.12", 12, 12),
784        ("PostgreSQL 13.8", 13, 8),
785        ("PostgreSQL 14.5", 14, 5),
786        ("PostgreSQL 11.2-FOO-BAR+", 11, 2),
787        ("PostgreSQL 10.22-", 10, 22),
788    ];
789    for (s, major_expected, minor_expected) in versions {
790        let (major, minor) =
791            PgConfig::parse_version_str(s).expect("Unable to parse version string");
792        assert_eq!(major, major_expected, "Major version should match");
793        assert_eq!(minor.version(), Some(minor_expected), "Minor version should match");
794    }
795
796    // Check some invalid version strings
797    let _ = PgConfig::parse_version_str("10.22").expect_err("Parsed invalid version string");
798    let _ =
799        PgConfig::parse_version_str("PostgresSQL 10").expect_err("Parsed invalid version string");
800    let _ =
801        PgConfig::parse_version_str("PostgresSQL 10.").expect_err("Parsed invalid version string");
802    let _ =
803        PgConfig::parse_version_str("PostgresSQL 12.f").expect_err("Parsed invalid version string");
804    let _ =
805        PgConfig::parse_version_str("PostgresSQL .53").expect_err("Parsed invalid version string");
806}
807
808#[test]
809fn from_empty_env() -> eyre::Result<()> {
810    // without "PGRX_PG_CONFIG_AS_ENV" we can't get one of these
811    let pg_config = PgConfig::from_env();
812    assert!(pg_config.is_err());
813
814    // but now we can
815    std::env::set_var("PGRX_PG_CONFIG_AS_ENV", "true");
816    std::env::set_var("PGRX_PG_CONFIG_VERSION", "PostgresSQL 15.1");
817    std::env::set_var("PGRX_PG_CONFIG_INCLUDEDIR-SERVER", "/path/to/server/headers");
818    std::env::set_var("PGRX_PG_CONFIG_CPPFLAGS", "some cpp flags");
819
820    let pg_config = PgConfig::from_env().unwrap();
821    assert_eq!(pg_config.major_version()?, 15, "Major version should match");
822    assert_eq!(
823        pg_config.minor_version()?,
824        PgMinorVersion::Release(1),
825        "Minor version should match"
826    );
827    assert_eq!(
828        pg_config.includedir_server()?,
829        PathBuf::from("/path/to/server/headers"),
830        "includdir_server should match"
831    );
832    assert_eq!(pg_config.cppflags()?, OsString::from("some cpp flags"), "cppflags should match");
833
834    // we didn't set this one in our environment
835    assert!(pg_config.sharedir().is_err());
836    Ok(())
837}