Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions sqlx-postgres/src/options/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ if a parameter is not passed in via URL, it is populated by reading
| `options` | `PGOPTIONS` | Unset. |
| `application_name` | `PGAPPNAME` | Unset. |

[`passfile`] handling may be bypassed using [`PgConnectOptions::new_without_pgpass()`].
[`passfile`] handling may be bypassed using [`PgConnectOptions::default_without_env()`],
which also bypasses all environment variables and uses hardcoded defaults.

## SQLx-Specific
SQLx also parses some bespoke parameters. These are _not_ configurable by environment variable.
Expand Down Expand Up @@ -154,8 +155,8 @@ use sqlx::postgres::{PgConnectOptions, PgConnection, PgPool, PgSslMode};
// URL connection string
let conn = PgConnection::connect("postgres://localhost/mydb").await?;

// Manually-constructed options
let conn = PgConnectOptions::new()
// Manually-constructed options with environment defaults
let conn = PgConnectOptions::with_libpq_defaults()
.host("secret-host")
.port(2525)
.username("secret-user")
Expand All @@ -164,6 +165,14 @@ let conn = PgConnectOptions::new()
.connect()
.await?;

// Or start from hardcoded defaults without environment variables
let conn = PgConnectOptions::default_without_env()
.host("secret-host")
.username("secret-user")
.password("secret-password")
.connect()
.await?;

// Modifying options parsed from a string
let mut opts: PgConnectOptions = "postgres://localhost/mydb".parse()?;

Expand Down
191 changes: 179 additions & 12 deletions sqlx-postgres/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub struct PgConnectOptions {

impl Default for PgConnectOptions {
fn default() -> Self {
Self::new_without_pgpass().apply_pgpass()
Self::with_libpq_defaults()
}
}

Expand All @@ -44,16 +44,93 @@ impl PgConnectOptions {
/// This behaves as if parsed from the connection string `postgres://`
///
/// See the type-level documentation for details.
///
/// # Deprecated
/// This method is deprecated. Use [`with_libpq_defaults()`](Self::with_libpq_defaults) instead.
#[deprecated(
since = "0.9.0",
note = "Use `with_libpq_defaults()` instead to make the behavior more explicit"
)]
pub fn new() -> Self {
Self::new_without_pgpass().apply_pgpass()
Self::with_libpq_defaults()
}

/// Create a default set of connection options populated from the current environment,
/// mimicking libpq's default behavior.
///
/// This reads environment variables (`PGHOST`, `PGPORT`, `PGUSER`, etc.) and `.pgpass` file
/// to populate connection options, similar to how libpq behaves.
///
/// This behaves as if parsed from the connection string `postgres://`
///
/// See the type-level documentation for details.
pub fn with_libpq_defaults() -> Self {
Self::default_without_env_internal().apply_env_and_pgpass()
}

/// Create a default set of connection options _without_ reading from `passfile`.
///
/// Equivalent to [`PgConnectOptions::new()`] but `passfile` is ignored.
/// Equivalent to [`PgConnectOptions::with_libpq_defaults()`] but `passfile` is ignored.
///
/// See the type-level documentation for details.
///
/// # Deprecated
/// This method is deprecated. Use [`default_without_env()`](Self::default_without_env) instead.
#[deprecated(
since = "0.9.0",
note = "Use `default_without_env()` for explicit defaults without environment variables"
)]
pub fn new_without_pgpass() -> Self {
Self::default_without_env_internal().apply_env()
}

/// Create connection options with sensible defaults without reading environment variables.
///
/// This method provides a predictable baseline for connection options that doesn't depend
/// on the environment. Useful for developer tools, third-party libraries, or any context
/// where environment variables cannot be relied upon.
///
/// The defaults are:
/// - `host`: `"localhost"` (or Unix socket path if available)
/// - `port`: `5432`
/// - `username`: `"postgres"`
/// - `ssl_mode`: [`PgSslMode::Prefer`]
/// - `statement_cache_capacity`: `100`
/// - `extra_float_digits`: `Some("2")`
///
/// All other fields are set to `None`.
///
/// Does not respect any `PG*` environment variables or `.pgpass` files.
///
/// See the type-level documentation for details.
pub fn default_without_env() -> Self {
let port = 5432;
let host = default_host(port);
let username = "postgres".to_string();

PgConnectOptions {
host,
port,
socket: None,
username,
password: None,
database: None,
ssl_mode: PgSslMode::Prefer,
ssl_root_cert: None,
ssl_client_cert: None,
ssl_client_key: None,
statement_cache_capacity: 100,
application_name: None,
extra_float_digits: Some("2".into()),
log_settings: Default::default(),
options: None,
}
}

/// Internal method that reads environment variables (libpq-style).
///
/// This is used internally by `with_libpq_defaults()` and the deprecated `new_without_pgpass()`.
fn default_without_env_internal() -> Self {
let port = var("PGPORT")
.ok()
.and_then(|v| v.parse().ok())
Expand All @@ -68,23 +145,25 @@ impl PgConnectOptions {

let database = var("PGDATABASE").ok();

let ssl_mode = var("PGSSLMODE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_default();

PgConnectOptions {
port,
host,
port,
socket: None,
username,
password: var("PGPASSWORD").ok(),
database,
ssl_mode,
ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from),
ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from),
// As of writing, the implementation of `From<String>` only looks for
// `-----BEGIN CERTIFICATE-----` and so will not attempt to parse
// a PEM-encoded private key.
ssl_client_key: var("PGSSLKEY").ok().map(CertificateInput::from),
ssl_mode: var("PGSSLMODE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_default(),
statement_cache_capacity: 100,
application_name: var("PGAPPNAME").ok(),
extra_float_digits: Some("2".into()),
Expand All @@ -106,6 +185,24 @@ impl PgConnectOptions {
self
}

/// Apply environment variables only (no pgpass).
///
/// This is used internally for the deprecated `new_without_pgpass()`.
fn apply_env(self) -> Self {
// Environment variables are already applied in default_without_env_internal()
// This method exists for backwards compatibility but doesn't do anything
self
}

/// Apply both environment variables and pgpass.
///
/// This is used internally for `with_libpq_defaults()`.
fn apply_env_and_pgpass(self) -> Self {
// Environment variables are already applied in default_without_env_internal()
// We just need to apply pgpass
self.apply_pgpass()
}

/// Sets the name of the host to connect to.
///
/// If a host name begins with a slash, it specifies
Expand Down Expand Up @@ -629,26 +726,26 @@ impl Write for PgOptionsWriteEscaped<'_> {

#[test]
fn test_options_formatting() {
let options = PgConnectOptions::new().options([("geqo", "off")]);
let options = PgConnectOptions::default_without_env().options([("geqo", "off")]);
assert_eq!(options.options, Some("-c geqo=off".to_string()));
let options = options.options([("search_path", "sqlx")]);
assert_eq!(
options.options,
Some("-c geqo=off -c search_path=sqlx".to_string())
);
let options = PgConnectOptions::new().options([("geqo", "off"), ("statement_timeout", "5min")]);
let options = PgConnectOptions::default_without_env().options([("geqo", "off"), ("statement_timeout", "5min")]);
assert_eq!(
options.options,
Some("-c geqo=off -c statement_timeout=5min".to_string())
);
// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-OPTIONS
let options =
PgConnectOptions::new().options([("application_name", r"/back\slash/ and\ spaces")]);
PgConnectOptions::default_without_env().options([("application_name", r"/back\slash/ and\ spaces")]);
assert_eq!(
options.options,
Some(r"-c application_name=/back\\slash/\ and\\\ spaces".to_string())
);
let options = PgConnectOptions::new();
let options = PgConnectOptions::default_without_env();
assert_eq!(options.options, None);
}

Expand All @@ -664,3 +761,73 @@ fn test_pg_write_escaped() {
x.write_char('z').unwrap();
assert_eq!(buf, r"x\\y\ \\\ z");
}

#[test]
#[allow(deprecated)]
fn test_deprecated_api_backwards_compatibility() {
// Test that deprecated methods still work correctly for backwards compatibility

// Test deprecated new() method
let options = PgConnectOptions::new();
assert_eq!(options.port, 5432);
assert_eq!(options.statement_cache_capacity, 100);
assert!(options.extra_float_digits.is_some());

// Test deprecated new_without_pgpass() method
let options = PgConnectOptions::new_without_pgpass();
assert_eq!(options.port, 5432);
assert_eq!(options.statement_cache_capacity, 100);

// Verify the deprecated methods can be chained with builder methods
let options = PgConnectOptions::new()
.host("example.com")
.port(5433)
.username("testuser")
.database("testdb");

assert_eq!(options.get_host(), "example.com");
assert_eq!(options.get_port(), 5433);
assert_eq!(options.get_username(), "testuser");
assert_eq!(options.get_database(), Some("testdb"));

// Verify new_without_pgpass() works with builder pattern
let options = PgConnectOptions::new_without_pgpass()
.host("localhost")
.username("postgres");

assert_eq!(options.get_host(), "localhost");
assert_eq!(options.get_username(), "postgres");
}

#[test]
fn test_new_api_without_environment() {
// Test the new API methods that don't read environment variables

// Test default_without_env() provides hardcoded defaults
let options = PgConnectOptions::default_without_env();
assert_eq!(options.port, 5432);
assert_eq!(options.username, "postgres"); // Hardcoded, not OS username
assert_eq!(options.ssl_mode, PgSslMode::Prefer);
assert_eq!(options.statement_cache_capacity, 100);
assert!(options.extra_float_digits.is_some());
assert!(options.password.is_none());
assert!(options.database.is_none());

// Test builder pattern with default_without_env()
let options = PgConnectOptions::default_without_env()
.host("example.com")
.port(5433)
.username("myuser")
.database("mydb")
.password("mypass");

assert_eq!(options.get_host(), "example.com");
assert_eq!(options.get_port(), 5433);
assert_eq!(options.get_username(), "myuser");
assert_eq!(options.get_database(), Some("mydb"));

// Test with_libpq_defaults()
let options = PgConnectOptions::with_libpq_defaults();
assert_eq!(options.port, 5432);
assert_eq!(options.statement_cache_capacity, 100);
}
Loading
Loading