1use 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
29pub 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 _ => &["--locale=C"],
48 }
49 }
50}
51
52mod 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 let pg_config = PgConfig::from_env();
812 assert!(pg_config.is_err());
813
814 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 assert!(pg_config.sharedir().is_err());
836 Ok(())
837}