1use 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
32pub 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 _ => &["--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
59mod 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 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 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 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(); pgxs_path.pop(); 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 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 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 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 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 #[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 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 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 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 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 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 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 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 let pg_config = PgConfig::from_env();
924 assert!(pg_config.is_err());
925
926 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 assert!(pg_config.sharedir().is_err());
948 Ok(())
949}