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
25pub mod cargo;
26
27pub static BASE_POSTGRES_PORT_NO: u16 = 28800;
28pub static BASE_POSTGRES_TESTING_PORT_NO: u16 = 32200;
29
30pub 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 _ => &["--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
57mod 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 let pg_config = PgConfig::from_env();
835 assert!(pg_config.is_err());
836
837 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 assert!(pg_config.sharedir().is_err());
859 Ok(())
860}