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