sqlx_postgres/options/mod.rs
1use std::borrow::Cow;
2use std::env::var;
3use std::fmt::{self, Display, Write};
4use std::path::{Path, PathBuf};
5
6pub use ssl_mode::PgSslMode;
7
8use crate::{connection::LogSettings, net::tls::CertificateInput};
9
10mod connect;
11mod parse;
12mod pgpass;
13mod ssl_mode;
14
15#[doc = include_str!("doc.md")]
16#[derive(Debug, Clone)]
17pub struct PgConnectOptions {
18 pub(crate) host: String,
19 pub(crate) port: u16,
20 pub(crate) socket: Option<PathBuf>,
21 pub(crate) username: String,
22 pub(crate) password: Option<String>,
23 pub(crate) database: Option<String>,
24 pub(crate) ssl_mode: PgSslMode,
25 pub(crate) ssl_root_cert: Option<CertificateInput>,
26 pub(crate) ssl_client_cert: Option<CertificateInput>,
27 pub(crate) ssl_client_key: Option<CertificateInput>,
28 pub(crate) statement_cache_capacity: usize,
29 pub(crate) application_name: Option<String>,
30 pub(crate) log_settings: LogSettings,
31 pub(crate) extra_float_digits: Option<Cow<'static, str>>,
32 pub(crate) options: Option<String>,
33}
34
35impl Default for PgConnectOptions {
36 fn default() -> Self {
37 Self::new_without_pgpass().apply_pgpass()
38 }
39}
40
41impl PgConnectOptions {
42 /// Create a default set of connection options populated from the current environment.
43 ///
44 /// This behaves as if parsed from the connection string `postgres://`
45 ///
46 /// See the type-level documentation for details.
47 pub fn new() -> Self {
48 Self::new_without_pgpass().apply_pgpass()
49 }
50
51 /// Create a default set of connection options _without_ reading from `passfile`.
52 ///
53 /// Equivalent to [`PgConnectOptions::new()`] but `passfile` is ignored.
54 ///
55 /// See the type-level documentation for details.
56 pub fn new_without_pgpass() -> Self {
57 let port = var("PGPORT")
58 .ok()
59 .and_then(|v| v.parse().ok())
60 .unwrap_or(5432);
61
62 let host = var("PGHOSTADDR")
63 .ok()
64 .or_else(|| var("PGHOST").ok())
65 .unwrap_or_else(|| default_host(port));
66
67 let username = if let Ok(username) = var("PGUSER") {
68 username
69 } else if let Ok(username) = whoami::username() {
70 username
71 } else {
72 // keep the same fallback as previous version
73 "unknown".to_string()
74 };
75
76 let database = var("PGDATABASE").ok();
77
78 PgConnectOptions {
79 port,
80 host,
81 socket: None,
82 username,
83 password: var("PGPASSWORD").ok(),
84 database,
85 ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from),
86 ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from),
87 // As of writing, the implementation of `From<String>` only looks for
88 // `-----BEGIN CERTIFICATE-----` and so will not attempt to parse
89 // a PEM-encoded private key.
90 ssl_client_key: var("PGSSLKEY").ok().map(CertificateInput::from),
91 ssl_mode: var("PGSSLMODE")
92 .ok()
93 .and_then(|v| v.parse().ok())
94 .unwrap_or_default(),
95 statement_cache_capacity: 100,
96 application_name: var("PGAPPNAME").ok(),
97 extra_float_digits: Some("2".into()),
98 log_settings: Default::default(),
99 options: var("PGOPTIONS").ok(),
100 }
101 }
102
103 pub(crate) fn apply_pgpass(mut self) -> Self {
104 if self.password.is_none() {
105 self.password = pgpass::load_password(
106 &self.host,
107 self.port,
108 &self.username,
109 self.database.as_deref(),
110 );
111 }
112
113 self
114 }
115
116 /// Sets the name of the host to connect to.
117 ///
118 /// If a host name begins with a slash, it specifies
119 /// Unix-domain communication rather than TCP/IP communication; the value is the name of
120 /// the directory in which the socket file is stored.
121 ///
122 /// The default behavior when host is not specified, or is empty,
123 /// is to connect to a Unix-domain socket
124 ///
125 /// # Example
126 ///
127 /// ```rust
128 /// # use sqlx_postgres::PgConnectOptions;
129 /// let options = PgConnectOptions::new()
130 /// .host("localhost");
131 /// ```
132 pub fn host(mut self, host: &str) -> Self {
133 host.clone_into(&mut self.host);
134 self
135 }
136
137 /// Sets the port to connect to at the server host.
138 ///
139 /// The default port for PostgreSQL is `5432`.
140 ///
141 /// # Example
142 ///
143 /// ```rust
144 /// # use sqlx_postgres::PgConnectOptions;
145 /// let options = PgConnectOptions::new()
146 /// .port(5432);
147 /// ```
148 pub fn port(mut self, port: u16) -> Self {
149 self.port = port;
150 self
151 }
152
153 /// Sets a custom path to a directory containing a unix domain socket,
154 /// switching the connection method from TCP to the corresponding socket.
155 ///
156 /// By default set to `None`.
157 pub fn socket(mut self, path: impl AsRef<Path>) -> Self {
158 self.socket = Some(path.as_ref().to_path_buf());
159 self
160 }
161
162 /// Sets the username to connect as.
163 ///
164 /// Defaults to be the same as the operating system name of
165 /// the user running the application.
166 ///
167 /// # Example
168 ///
169 /// ```rust
170 /// # use sqlx_postgres::PgConnectOptions;
171 /// let options = PgConnectOptions::new()
172 /// .username("postgres");
173 /// ```
174 pub fn username(mut self, username: &str) -> Self {
175 username.clone_into(&mut self.username);
176 self
177 }
178
179 /// Sets the password to use if the server demands password authentication.
180 ///
181 /// # Example
182 ///
183 /// ```rust
184 /// # use sqlx_postgres::PgConnectOptions;
185 /// let options = PgConnectOptions::new()
186 /// .username("root")
187 /// .password("safe-and-secure");
188 /// ```
189 pub fn password(mut self, password: &str) -> Self {
190 self.password = Some(password.to_owned());
191 self
192 }
193
194 /// Sets the database name. Defaults to be the same as the user name.
195 ///
196 /// # Example
197 ///
198 /// ```rust
199 /// # use sqlx_postgres::PgConnectOptions;
200 /// let options = PgConnectOptions::new()
201 /// .database("postgres");
202 /// ```
203 pub fn database(mut self, database: &str) -> Self {
204 self.database = Some(database.to_owned());
205 self
206 }
207
208 /// Sets whether or with what priority a secure SSL TCP/IP connection will be negotiated
209 /// with the server.
210 ///
211 /// By default, the SSL mode is [`Prefer`](PgSslMode::Prefer), and the client will
212 /// first attempt an SSL connection but fallback to a non-SSL connection on failure.
213 ///
214 /// Ignored for Unix domain socket communication.
215 ///
216 /// # Example
217 ///
218 /// ```rust
219 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
220 /// let options = PgConnectOptions::new()
221 /// .ssl_mode(PgSslMode::Require);
222 /// ```
223 pub fn ssl_mode(mut self, mode: PgSslMode) -> Self {
224 self.ssl_mode = mode;
225 self
226 }
227
228 /// Sets the name of a file containing SSL certificate authority (CA) certificate(s).
229 /// If the file exists, the server's certificate will be verified to be signed by
230 /// one of these authorities.
231 ///
232 /// # Example
233 ///
234 /// ```rust
235 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
236 /// let options = PgConnectOptions::new()
237 /// // Providing a CA certificate with less than VerifyCa is pointless
238 /// .ssl_mode(PgSslMode::VerifyCa)
239 /// .ssl_root_cert("./ca-certificate.crt");
240 /// ```
241 pub fn ssl_root_cert(mut self, cert: impl AsRef<Path>) -> Self {
242 self.ssl_root_cert = Some(CertificateInput::File(cert.as_ref().to_path_buf()));
243 self
244 }
245
246 /// Sets the name of a file containing SSL client certificate.
247 ///
248 /// # Example
249 ///
250 /// ```rust
251 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
252 /// let options = PgConnectOptions::new()
253 /// // Providing a CA certificate with less than VerifyCa is pointless
254 /// .ssl_mode(PgSslMode::VerifyCa)
255 /// .ssl_client_cert("./client.crt");
256 /// ```
257 pub fn ssl_client_cert(mut self, cert: impl AsRef<Path>) -> Self {
258 self.ssl_client_cert = Some(CertificateInput::File(cert.as_ref().to_path_buf()));
259 self
260 }
261
262 /// Sets the SSL client certificate as a PEM-encoded byte slice.
263 ///
264 /// This should be an ASCII-encoded blob that starts with `-----BEGIN CERTIFICATE-----`.
265 ///
266 /// # Example
267 /// Note: embedding SSL certificates and keys in the binary is not advised.
268 /// This is for illustration purposes only.
269 ///
270 /// ```rust
271 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
272 ///
273 /// const CERT: &[u8] = b"\
274 /// -----BEGIN CERTIFICATE-----
275 /// <Certificate data here.>
276 /// -----END CERTIFICATE-----";
277 ///
278 /// let options = PgConnectOptions::new()
279 /// // Providing a CA certificate with less than VerifyCa is pointless
280 /// .ssl_mode(PgSslMode::VerifyCa)
281 /// .ssl_client_cert_from_pem(CERT);
282 /// ```
283 pub fn ssl_client_cert_from_pem(mut self, cert: impl AsRef<[u8]>) -> Self {
284 self.ssl_client_cert = Some(CertificateInput::Inline(cert.as_ref().to_vec()));
285 self
286 }
287
288 /// Sets the name of a file containing SSL client key.
289 ///
290 /// # Example
291 ///
292 /// ```rust
293 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
294 /// let options = PgConnectOptions::new()
295 /// // Providing a CA certificate with less than VerifyCa is pointless
296 /// .ssl_mode(PgSslMode::VerifyCa)
297 /// .ssl_client_key("./client.key");
298 /// ```
299 pub fn ssl_client_key(mut self, key: impl AsRef<Path>) -> Self {
300 self.ssl_client_key = Some(CertificateInput::File(key.as_ref().to_path_buf()));
301 self
302 }
303
304 /// Sets the SSL client key as a PEM-encoded byte slice.
305 ///
306 /// This should be an ASCII-encoded blob that starts with `-----BEGIN PRIVATE KEY-----`.
307 ///
308 /// # Example
309 /// Note: embedding SSL certificates and keys in the binary is not advised.
310 /// This is for illustration purposes only.
311 ///
312 /// ```rust
313 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
314 ///
315 /// const KEY: &[u8] = b"\
316 /// -----BEGIN PRIVATE KEY-----
317 /// <Private key data here.>
318 /// -----END PRIVATE KEY-----";
319 ///
320 /// let options = PgConnectOptions::new()
321 /// // Providing a CA certificate with less than VerifyCa is pointless
322 /// .ssl_mode(PgSslMode::VerifyCa)
323 /// .ssl_client_key_from_pem(KEY);
324 /// ```
325 pub fn ssl_client_key_from_pem(mut self, key: impl AsRef<[u8]>) -> Self {
326 self.ssl_client_key = Some(CertificateInput::Inline(key.as_ref().to_vec()));
327 self
328 }
329
330 /// Sets PEM encoded trusted SSL Certificate Authorities (CA).
331 ///
332 /// # Example
333 ///
334 /// ```rust
335 /// # use sqlx_postgres::{PgSslMode, PgConnectOptions};
336 /// let options = PgConnectOptions::new()
337 /// // Providing a CA certificate with less than VerifyCa is pointless
338 /// .ssl_mode(PgSslMode::VerifyCa)
339 /// .ssl_root_cert_from_pem(vec![]);
340 /// ```
341 pub fn ssl_root_cert_from_pem(mut self, pem_certificate: Vec<u8>) -> Self {
342 self.ssl_root_cert = Some(CertificateInput::Inline(pem_certificate));
343 self
344 }
345
346 /// Sets the capacity of the connection's statement cache in a number of stored
347 /// distinct statements. Caching is handled using LRU, meaning when the
348 /// amount of queries hits the defined limit, the oldest statement will get
349 /// dropped.
350 ///
351 /// The default cache capacity is 100 statements.
352 pub fn statement_cache_capacity(mut self, capacity: usize) -> Self {
353 self.statement_cache_capacity = capacity;
354 self
355 }
356
357 /// Sets the application name. Defaults to None
358 ///
359 /// # Example
360 ///
361 /// ```rust
362 /// # use sqlx_postgres::PgConnectOptions;
363 /// let options = PgConnectOptions::new()
364 /// .application_name("my-app");
365 /// ```
366 pub fn application_name(mut self, application_name: &str) -> Self {
367 self.application_name = Some(application_name.to_owned());
368 self
369 }
370
371 /// Sets or removes the `extra_float_digits` connection option.
372 ///
373 /// This changes the default precision of floating-point values returned in text mode (when
374 /// not using prepared statements such as calling methods of [`Executor`] directly).
375 ///
376 /// Historically, Postgres would by default round floating-point values to 6 and 15 digits
377 /// for `float4`/`REAL` (`f32`) and `float8`/`DOUBLE` (`f64`), respectively, which would mean
378 /// that the returned value may not be exactly the same as its representation in Postgres.
379 ///
380 /// The nominal range for this value is `-15` to `3`, where negative values for this option
381 /// cause floating-points to be rounded to that many fewer digits than normal (`-1` causes
382 /// `float4` to be rounded to 5 digits instead of six, or 14 instead of 15 for `float8`),
383 /// positive values cause Postgres to emit that many extra digits of precision over default
384 /// (or simply use maximum precision in Postgres 12 and later),
385 /// and 0 means keep the default behavior (or the "old" behavior described above
386 /// as of Postgres 12).
387 ///
388 /// SQLx sets this value to 3 by default, which tells Postgres to return floating-point values
389 /// at their maximum precision in the hope that the parsed value will be identical to its
390 /// counterpart in Postgres. This is also the default in Postgres 12 and later anyway.
391 ///
392 /// However, older versions of Postgres and alternative implementations that talk the Postgres
393 /// protocol may not support this option, or the full range of values.
394 ///
395 /// If you get an error like "unknown option `extra_float_digits`" when connecting, try
396 /// setting this to `None` or consult the manual of your database for the allowed range
397 /// of values.
398 ///
399 /// For more information, see:
400 /// * [Postgres manual, 20.11.2: Client Connection Defaults; Locale and Formatting][20.11.2]
401 /// * [Postgres manual, 8.1.3: Numeric Types; Floating-point Types][8.1.3]
402 ///
403 /// [`Executor`]: crate::executor::Executor
404 /// [20.11.2]: https://www.postgresql.org/docs/current/runtime-config-client.html#RUNTIME-CONFIG-CLIENT-FORMAT
405 /// [8.1.3]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-FLOAT
406 ///
407 /// ### Examples
408 /// ```rust
409 /// # use sqlx_postgres::PgConnectOptions;
410 ///
411 /// let mut options = PgConnectOptions::new()
412 /// // for Redshift and Postgres 10
413 /// .extra_float_digits(2);
414 ///
415 /// let mut options = PgConnectOptions::new()
416 /// // don't send the option at all (Postgres 9 and older)
417 /// .extra_float_digits(None);
418 /// ```
419 pub fn extra_float_digits(mut self, extra_float_digits: impl Into<Option<i8>>) -> Self {
420 self.extra_float_digits = extra_float_digits.into().map(|it| it.to_string().into());
421 self
422 }
423
424 /// Set additional startup options for the connection as a list of key-value pairs.
425 ///
426 /// Escapes the options’ backslash and space characters as per
427 /// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS
428 ///
429 /// # Example
430 ///
431 /// ```rust
432 /// # use sqlx_postgres::PgConnectOptions;
433 /// let options = PgConnectOptions::new()
434 /// .options([("geqo", "off"), ("statement_timeout", "5min")]);
435 /// ```
436 pub fn options<K, V, I>(mut self, options: I) -> Self
437 where
438 K: Display,
439 V: Display,
440 I: IntoIterator<Item = (K, V)>,
441 {
442 // Do this in here so `options_str` is only set if we have an option to insert
443 let options_str = self.options.get_or_insert_with(String::new);
444 for (k, v) in options {
445 if !options_str.is_empty() {
446 options_str.push(' ');
447 }
448
449 options_str.push_str("-c ");
450 write!(PgOptionsWriteEscaped(options_str), "{k}={v}").ok();
451 }
452 self
453 }
454
455 /// We try using a socket if hostname starts with `/` or if socket parameter
456 /// is specified.
457 pub(crate) fn fetch_socket(&self) -> Option<String> {
458 match self.socket {
459 Some(ref socket) => {
460 let full_path = format!("{}/.s.PGSQL.{}", socket.display(), self.port);
461 Some(full_path)
462 }
463 None if self.host.starts_with('/') => {
464 let full_path = format!("{}/.s.PGSQL.{}", self.host, self.port);
465 Some(full_path)
466 }
467 _ => None,
468 }
469 }
470}
471
472impl PgConnectOptions {
473 /// Get the current host.
474 ///
475 /// # Example
476 ///
477 /// ```rust
478 /// # use sqlx_postgres::PgConnectOptions;
479 /// let options = PgConnectOptions::new()
480 /// .host("127.0.0.1");
481 /// assert_eq!(options.get_host(), "127.0.0.1");
482 /// ```
483 pub fn get_host(&self) -> &str {
484 &self.host
485 }
486
487 /// Get the server's port.
488 ///
489 /// # Example
490 ///
491 /// ```rust
492 /// # use sqlx_postgres::PgConnectOptions;
493 /// let options = PgConnectOptions::new()
494 /// .port(6543);
495 /// assert_eq!(options.get_port(), 6543);
496 /// ```
497 pub fn get_port(&self) -> u16 {
498 self.port
499 }
500
501 /// Get the socket path.
502 ///
503 /// # Example
504 ///
505 /// ```rust
506 /// # use sqlx_postgres::PgConnectOptions;
507 /// let options = PgConnectOptions::new()
508 /// .socket("/tmp");
509 /// assert!(options.get_socket().is_some());
510 /// ```
511 pub fn get_socket(&self) -> Option<&PathBuf> {
512 self.socket.as_ref()
513 }
514
515 /// Get the server's port.
516 ///
517 /// # Example
518 ///
519 /// ```rust
520 /// # use sqlx_postgres::PgConnectOptions;
521 /// let options = PgConnectOptions::new()
522 /// .username("foo");
523 /// assert_eq!(options.get_username(), "foo");
524 /// ```
525 pub fn get_username(&self) -> &str {
526 &self.username
527 }
528
529 /// Get the current database name.
530 ///
531 /// # Example
532 ///
533 /// ```rust
534 /// # use sqlx_postgres::PgConnectOptions;
535 /// let options = PgConnectOptions::new()
536 /// .database("postgres");
537 /// assert!(options.get_database().is_some());
538 /// ```
539 pub fn get_database(&self) -> Option<&str> {
540 self.database.as_deref()
541 }
542
543 /// Get the SSL mode.
544 ///
545 /// # Example
546 ///
547 /// ```rust
548 /// # use sqlx_postgres::{PgConnectOptions, PgSslMode};
549 /// let options = PgConnectOptions::new();
550 /// assert!(matches!(options.get_ssl_mode(), PgSslMode::Prefer));
551 /// ```
552 pub fn get_ssl_mode(&self) -> PgSslMode {
553 self.ssl_mode
554 }
555
556 /// Get the application name.
557 ///
558 /// # Example
559 ///
560 /// ```rust
561 /// # use sqlx_postgres::PgConnectOptions;
562 /// let options = PgConnectOptions::new()
563 /// .application_name("service");
564 /// assert!(options.get_application_name().is_some());
565 /// ```
566 pub fn get_application_name(&self) -> Option<&str> {
567 self.application_name.as_deref()
568 }
569
570 /// Get the options.
571 ///
572 /// # Example
573 ///
574 /// ```rust
575 /// # use sqlx_postgres::PgConnectOptions;
576 /// let options = PgConnectOptions::new()
577 /// .options([("foo", "bar")]);
578 /// assert!(options.get_options().is_some());
579 /// ```
580 pub fn get_options(&self) -> Option<&str> {
581 self.options.as_deref()
582 }
583}
584
585fn default_host(port: u16) -> String {
586 // try to check for the existence of a unix socket and uses that
587 let socket = format!(".s.PGSQL.{port}");
588 let candidates = [
589 "/var/run/postgresql", // Debian
590 "/private/tmp", // OSX (homebrew)
591 "/tmp", // Default
592 ];
593
594 for candidate in &candidates {
595 if Path::new(candidate).join(&socket).exists() {
596 return candidate.to_string();
597 }
598 }
599
600 // fallback to localhost if no socket was found
601 "localhost".to_owned()
602}
603
604/// Writer that escapes passed-in PostgreSQL options.
605///
606/// Escapes backslashes and spaces with an additional backslash according to
607/// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS
608#[derive(Debug)]
609struct PgOptionsWriteEscaped<'a>(&'a mut String);
610
611impl Write for PgOptionsWriteEscaped<'_> {
612 fn write_str(&mut self, s: &str) -> fmt::Result {
613 let mut span_start = 0;
614
615 for (span_end, matched) in s.match_indices([' ', '\\']) {
616 write!(self.0, r"{}\{matched}", &s[span_start..span_end])?;
617 span_start = span_end + matched.len();
618 }
619
620 // Write the rest of the string after the last match, or all of it if no matches
621 self.0.push_str(&s[span_start..]);
622
623 Ok(())
624 }
625
626 fn write_char(&mut self, ch: char) -> fmt::Result {
627 if matches!(ch, ' ' | '\\') {
628 self.0.push('\\');
629 }
630
631 self.0.push(ch);
632
633 Ok(())
634 }
635}
636
637#[test]
638fn test_options_formatting() {
639 let options = PgConnectOptions::new().options([("geqo", "off")]);
640 assert_eq!(options.options, Some("-c geqo=off".to_string()));
641 let options = options.options([("search_path", "sqlx")]);
642 assert_eq!(
643 options.options,
644 Some("-c geqo=off -c search_path=sqlx".to_string())
645 );
646 let options = PgConnectOptions::new().options([("geqo", "off"), ("statement_timeout", "5min")]);
647 assert_eq!(
648 options.options,
649 Some("-c geqo=off -c statement_timeout=5min".to_string())
650 );
651 // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS
652 let options =
653 PgConnectOptions::new().options([("application_name", r"/back\slash/ and\ spaces")]);
654 assert_eq!(
655 options.options,
656 Some(r"-c application_name=/back\\slash/\ and\\\ spaces".to_string())
657 );
658 let options = PgConnectOptions::new();
659 assert_eq!(options.options, None);
660}
661
662#[test]
663fn test_pg_write_escaped() {
664 let mut buf = String::new();
665 let mut x = PgOptionsWriteEscaped(&mut buf);
666 x.write_str("x").unwrap();
667 x.write_str("").unwrap();
668 x.write_char('\\').unwrap();
669 x.write_str("y \\").unwrap();
670 x.write_char(' ').unwrap();
671 x.write_char('z').unwrap();
672 assert_eq!(buf, r"x\\y\ \\\ z");
673}