Skip to content

Commit 371bded

Browse files
committed
feat: support standard Postgres env vars for db connection
Support DATABASE_URL, PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE environment variables for configuring the database connection. Env vars take highest priority, overriding CLI args, LSP settings, and config file values. Closes #302
1 parent f439d7f commit 371bded

3 files changed

Lines changed: 218 additions & 6 deletions

File tree

crates/pgls_configuration/src/database.rs

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,29 @@ use serde::{Deserialize, Serialize};
1111
pub struct DatabaseConfiguration {
1212
/// A connection string that encodes the full connection setup.
1313
/// When provided, it takes precedence over the individual fields.
14-
#[partial(bpaf(long("connection-string")))]
14+
#[partial(bpaf(env("DATABASE_URL"), long("connection-string")))]
1515
pub connection_string: Option<String>,
1616

1717
/// The host of the database.
1818
/// Required if you want database-related features.
1919
/// All else falls back to sensible defaults.
20-
#[partial(bpaf(long("host")))]
20+
#[partial(bpaf(env("PGHOST"), long("host")))]
2121
pub host: String,
2222

2323
/// The port of the database.
24-
#[partial(bpaf(long("port")))]
24+
#[partial(bpaf(env("PGPORT"), long("port")))]
2525
pub port: u16,
2626

2727
/// The username to connect to the database.
28-
#[partial(bpaf(long("username")))]
28+
#[partial(bpaf(env("PGUSER"), long("username")))]
2929
pub username: String,
3030

3131
/// The password to connect to the database.
32-
#[partial(bpaf(long("password")))]
32+
#[partial(bpaf(env("PGPASSWORD"), long("password")))]
3333
pub password: String,
3434

3535
/// The name of the database.
36-
#[partial(bpaf(long("database")))]
36+
#[partial(bpaf(env("PGDATABASE"), long("database")))]
3737
pub database: String,
3838

3939
#[partial(bpaf(long("allow_statement_executions_against")))]
@@ -64,3 +64,156 @@ impl Default for DatabaseConfiguration {
6464
}
6565
}
6666
}
67+
68+
impl PartialDatabaseConfiguration {
69+
/// Creates a partial configuration from standard Postgres environment variables.
70+
///
71+
/// Reads `DATABASE_URL`, `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, and `PGDATABASE`.
72+
/// Returns `None` if no relevant env vars are set.
73+
pub fn from_env() -> Option<Self> {
74+
let database_url = std::env::var("DATABASE_URL").ok();
75+
let pghost = std::env::var("PGHOST").ok();
76+
let pgport = std::env::var("PGPORT")
77+
.ok()
78+
.and_then(|p| p.parse().ok());
79+
let pguser = std::env::var("PGUSER").ok();
80+
let pgpassword = std::env::var("PGPASSWORD").ok();
81+
let pgdatabase = std::env::var("PGDATABASE").ok();
82+
83+
let has_any = database_url.is_some()
84+
|| pghost.is_some()
85+
|| pgport.is_some()
86+
|| pguser.is_some()
87+
|| pgpassword.is_some()
88+
|| pgdatabase.is_some();
89+
90+
if !has_any {
91+
return None;
92+
}
93+
94+
Some(Self {
95+
connection_string: database_url,
96+
host: pghost,
97+
port: pgport,
98+
username: pguser,
99+
password: pgpassword,
100+
database: pgdatabase,
101+
..Default::default()
102+
})
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use super::*;
109+
use std::sync::Mutex;
110+
111+
static ENV_MUTEX: Mutex<()> = Mutex::new(());
112+
113+
const ALL_VARS: &[&str] = &[
114+
"DATABASE_URL",
115+
"PGHOST",
116+
"PGPORT",
117+
"PGUSER",
118+
"PGPASSWORD",
119+
"PGDATABASE",
120+
];
121+
122+
fn clear_env_vars() {
123+
for var in ALL_VARS {
124+
unsafe {
125+
std::env::remove_var(var);
126+
}
127+
}
128+
}
129+
130+
#[test]
131+
fn from_env_none_when_no_vars_set() {
132+
let _lock = ENV_MUTEX.lock().unwrap();
133+
clear_env_vars();
134+
135+
assert!(PartialDatabaseConfiguration::from_env().is_none());
136+
}
137+
138+
#[test]
139+
fn from_env_all_vars_set() {
140+
let _lock = ENV_MUTEX.lock().unwrap();
141+
clear_env_vars();
142+
143+
unsafe {
144+
std::env::set_var("DATABASE_URL", "postgres://u:p@h:1234/d");
145+
std::env::set_var("PGHOST", "myhost");
146+
std::env::set_var("PGPORT", "5433");
147+
std::env::set_var("PGUSER", "myuser");
148+
std::env::set_var("PGPASSWORD", "mypass");
149+
std::env::set_var("PGDATABASE", "mydb");
150+
}
151+
152+
let config = PartialDatabaseConfiguration::from_env().unwrap();
153+
assert_eq!(
154+
config.connection_string,
155+
Some("postgres://u:p@h:1234/d".to_string())
156+
);
157+
assert_eq!(config.host, Some("myhost".to_string()));
158+
assert_eq!(config.port, Some(5433));
159+
assert_eq!(config.username, Some("myuser".to_string()));
160+
assert_eq!(config.password, Some("mypass".to_string()));
161+
assert_eq!(config.database, Some("mydb".to_string()));
162+
163+
clear_env_vars();
164+
}
165+
166+
#[test]
167+
fn from_env_partial_vars() {
168+
let _lock = ENV_MUTEX.lock().unwrap();
169+
clear_env_vars();
170+
171+
unsafe {
172+
std::env::set_var("PGHOST", "remotehost");
173+
std::env::set_var("PGDATABASE", "appdb");
174+
}
175+
176+
let config = PartialDatabaseConfiguration::from_env().unwrap();
177+
assert_eq!(config.connection_string, None);
178+
assert_eq!(config.host, Some("remotehost".to_string()));
179+
assert_eq!(config.port, None);
180+
assert_eq!(config.username, None);
181+
assert_eq!(config.password, None);
182+
assert_eq!(config.database, Some("appdb".to_string()));
183+
184+
clear_env_vars();
185+
}
186+
187+
#[test]
188+
fn from_env_invalid_pgport_ignored() {
189+
let _lock = ENV_MUTEX.lock().unwrap();
190+
clear_env_vars();
191+
192+
unsafe {
193+
std::env::set_var("PGHOST", "localhost");
194+
std::env::set_var("PGPORT", "not_a_number");
195+
}
196+
197+
let config = PartialDatabaseConfiguration::from_env().unwrap();
198+
assert_eq!(config.host, Some("localhost".to_string()));
199+
assert_eq!(config.port, None);
200+
201+
clear_env_vars();
202+
}
203+
204+
#[test]
205+
fn from_env_only_invalid_pgport_returns_none() {
206+
let _lock = ENV_MUTEX.lock().unwrap();
207+
clear_env_vars();
208+
209+
unsafe {
210+
std::env::set_var("PGPORT", "not_a_number");
211+
}
212+
213+
// PGPORT is set but invalid — parse fails so it becomes None.
214+
// No other vars are set, so has_any is false.
215+
assert!(PartialDatabaseConfiguration::from_env().is_none());
216+
217+
clear_env_vars();
218+
}
219+
}

crates/pgls_env/src/lib.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ pub struct PgLSEnv {
3535
pub pgls_log_prefix: PgLSEnvVariable,
3636
pub pgls_config_path: PgLSEnvVariable,
3737

38+
// Standard Postgres connection env vars
39+
pub database_url: PgLSEnvVariable,
40+
pub pghost: PgLSEnvVariable,
41+
pub pgport: PgLSEnvVariable,
42+
pub pguser: PgLSEnvVariable,
43+
pub pgpassword: PgLSEnvVariable,
44+
pub pgdatabase: PgLSEnvVariable,
45+
3846
// DEPRECATED - kept for backward compatibility
3947
pub pgt_log_path: PgLSEnvVariable,
4048
pub pgt_log_level: PgLSEnvVariable,
@@ -64,6 +72,31 @@ impl PgLSEnv {
6472
"A path to the configuration file",
6573
),
6674

75+
database_url: PgLSEnvVariable::new(
76+
"DATABASE_URL",
77+
"A connection string that encodes the full database connection setup.",
78+
),
79+
pghost: PgLSEnvVariable::new(
80+
"PGHOST",
81+
"The host of the database server.",
82+
),
83+
pgport: PgLSEnvVariable::new(
84+
"PGPORT",
85+
"The port of the database server.",
86+
),
87+
pguser: PgLSEnvVariable::new(
88+
"PGUSER",
89+
"The username to connect to the database.",
90+
),
91+
pgpassword: PgLSEnvVariable::new(
92+
"PGPASSWORD",
93+
"The password to connect to the database.",
94+
),
95+
pgdatabase: PgLSEnvVariable::new(
96+
"PGDATABASE",
97+
"The name of the database to connect to.",
98+
),
99+
67100
pgt_log_path: PgLSEnvVariable::new(
68101
"PGT_LOG_PATH",
69102
"The directory where the Daemon logs will be saved. Deprecated, use PGLS_LOG_PATH instead.",
@@ -156,6 +189,24 @@ impl Display for PgLSEnv {
156189
}
157190
};
158191

192+
for var in [
193+
&self.database_url,
194+
&self.pghost,
195+
&self.pgport,
196+
&self.pguser,
197+
&self.pgpassword,
198+
&self.pgdatabase,
199+
] {
200+
match var.value() {
201+
None => {
202+
KeyValuePair(var.name, markup! { <Dim>"unset"</Dim> }).fmt(fmt)?;
203+
}
204+
Some(value) => {
205+
KeyValuePair(var.name, markup! {{DebugDisplay(value)}}).fmt(fmt)?;
206+
}
207+
};
208+
}
209+
159210
match self.pgt_log_path.value() {
160211
None => {
161212
KeyValuePair(self.pgt_log_path.name, markup! { <Dim>"unset"</Dim> }).fmt(fmt)?;

crates/pgls_lsp/src/session.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use biome_deserialize::Merge;
77
use futures::StreamExt;
88
use futures::stream::FuturesUnordered;
99
use pgls_analyse::RuleCategoriesBuilder;
10+
use pgls_configuration::database::PartialDatabaseConfiguration;
1011
use pgls_configuration::{ConfigurationPathHint, PartialConfiguration};
1112
use pgls_diagnostics::{DiagnosticExt, Error};
1213
use pgls_fs::{ConfigName, FileSystem, PgLSPath};
@@ -499,6 +500,13 @@ impl Session {
499500
fs_configuration.merge_with(ws_configuration);
500501
}
501502

503+
if let Some(env_db) = PartialDatabaseConfiguration::from_env() {
504+
match &mut fs_configuration.db {
505+
Some(db) => db.merge_with(env_db),
506+
None => fs_configuration.db = Some(env_db),
507+
}
508+
}
509+
502510
let result = fs_configuration
503511
.retrieve_gitignore_matches(&self.fs, configuration_path.as_deref());
504512

0 commit comments

Comments
 (0)