Skip to content

Commit d89118d

Browse files
authored
feat: support standard postgres env vars for db connection (#674)
Support `DATABASE_URL`, `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` env vars for configuring the database connection with highest priority. we use bpaf(env()) for the cli, and support loading the db config from_env for the lsp. These env var names follow the standard [libpq environment variables](https://www.postgresql.org/docs/current/libpq-envars.html) (`PG*`) and the widely adopted `DATABASE_URL` convention used by tools like `sqlx`, `diesel`, `prisma`, and `dotenv`. ### Example ```sh DATABASE_URL=postgres://user:pass@localhost:5432/mydb postgres-language-server check file.sql # or individual vars PGHOST=localhost PGPORT=5432 PGUSER=user PGPASSWORD=pass PGDATABASE=mydb postgres-language-server check file.sql ``` ### Precedence (highest to lowest) 1. Environment variables 2. CLI args / LSP client settings 3. Config file 4. Defaults Closes #302
1 parent 4b3ff6b commit d89118d

15 files changed

Lines changed: 290 additions & 32 deletions

File tree

.github/workflows/publish.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ jobs:
9797
9898
- name: Setup Bun
9999
uses: oven-sh/setup-bun@v2
100+
with:
101+
bun-version: "1.2.17"
100102

101103
- name: Install dependencies
102104
run: bun install

.github/workflows/pull_request.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ jobs:
4949

5050
- name: Setup Bun
5151
uses: oven-sh/setup-bun@v2
52+
with:
53+
bun-version: "1.2.17"
5254

5355
- name: Install JS dependencies
5456
run: bun install
@@ -140,6 +142,8 @@ jobs:
140142

141143
- name: Setup Bun
142144
uses: oven-sh/setup-bun@v2
145+
with:
146+
bun-version: "1.2.17"
143147

144148
- name: Install JS dependencies
145149
run: bun install
@@ -239,6 +243,8 @@ jobs:
239243
run: cargo build -p pgls_cli --release
240244
- name: Setup Bun
241245
uses: oven-sh/setup-bun@v2
246+
with:
247+
bun-version: "1.2.17"
242248
- name: Install JS dependencies
243249
run: bun install
244250

bun.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pgls_cli/src/commands/mod.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,10 @@ pub enum PgLSCommand {
196196
/// Writes to stdout by default, or to a file if --output is specified.
197197
#[bpaf(command("schema-export"))]
198198
SchemaExport {
199-
/// PostgreSQL connection string (e.g., postgres://user:pass@host/db)
200-
#[bpaf(
201-
env("DATABASE_URL"),
202-
long("connection-string"),
203-
short('c'),
204-
argument("URL")
205-
)]
206-
connection_string: String,
199+
/// PostgreSQL connection string (e.g., postgres://user:pass@host/db).
200+
/// Can also be set via the `DATABASE_URL` environment variable.
201+
#[bpaf(long("connection-string"), short('c'), argument("URL"), optional)]
202+
connection_string: Option<String>,
207203

208204
/// Output file path for the JSON schema (defaults to stdout if not specified)
209205
#[bpaf(long("output"), short('o'), argument("PATH"))]

crates/pgls_cli/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ impl<'app> CliSession<'app> {
144144
connection_string,
145145
output,
146146
} => {
147+
let connection_string = connection_string
148+
.or_else(|| std::env::var("DATABASE_URL").ok())
149+
.ok_or_else(|| {
150+
CliDiagnostic::missing_argument(
151+
"--connection-string (or DATABASE_URL env var)",
152+
"schema-export",
153+
)
154+
})?;
147155
let runtime = tokio::runtime::Runtime::new().map_err(CliDiagnostic::io_error)?;
148156
runtime.block_on(commands::schema_export::run_schema_export(
149157
&connection_string,
@@ -193,6 +201,17 @@ impl<'app> CliSession<'app> {
193201
}
194202

195203
let mut configuration = loaded_configuration.configuration;
204+
205+
// Env vars override config file but are overridden by explicit CLI args.
206+
if let Some(env_db) = pgls_configuration::database::PartialDatabaseConfiguration::from_env()
207+
{
208+
let env_config = PartialConfiguration {
209+
db: Some(env_db),
210+
..Default::default()
211+
};
212+
configuration.merge_with(env_config);
213+
}
214+
196215
if let Some(cli_config) = cli_configuration {
197216
configuration.merge_with(cli_config);
198217
}

crates/pgls_configuration/src/database.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,41 @@ use bpaf::Bpaf;
44
use serde::{Deserialize, Serialize};
55

66
/// The configuration of the database connection.
7-
#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)]
7+
#[derive(Clone, Deserialize, Eq, Partial, PartialEq, Serialize)]
88
#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))]
99
#[partial(cfg_attr(feature = "schema", derive(schemars::JsonSchema)))]
1010
#[partial(serde(rename_all = "camelCase", default, deny_unknown_fields))]
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+
/// Can also be set via the `DATABASE_URL` environment variable.
1415
#[partial(bpaf(long("connection-string")))]
1516
pub connection_string: Option<String>,
1617

1718
/// The host of the database.
1819
/// Required if you want database-related features.
1920
/// All else falls back to sensible defaults.
21+
/// Can also be set via the `PGHOST` environment variable.
2022
#[partial(bpaf(long("host")))]
2123
pub host: String,
2224

2325
/// The port of the database.
26+
/// Can also be set via the `PGPORT` environment variable.
2427
#[partial(bpaf(long("port")))]
2528
pub port: u16,
2629

2730
/// The username to connect to the database.
31+
/// Can also be set via the `PGUSER` environment variable.
2832
#[partial(bpaf(long("username")))]
2933
pub username: String,
3034

3135
/// The password to connect to the database.
36+
/// Can also be set via the `PGPASSWORD` environment variable.
3237
#[partial(bpaf(long("password")))]
3338
pub password: String,
3439

3540
/// The name of the database.
41+
/// Can also be set via the `PGDATABASE` environment variable.
3642
#[partial(bpaf(long("database")))]
3743
pub database: String,
3844

@@ -49,6 +55,28 @@ pub struct DatabaseConfiguration {
4955
pub disable_connection: bool,
5056
}
5157

58+
impl std::fmt::Debug for DatabaseConfiguration {
59+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60+
f.debug_struct("DatabaseConfiguration")
61+
.field(
62+
"connection_string",
63+
&self.connection_string.as_ref().map(|_| "[redacted]"),
64+
)
65+
.field("host", &self.host)
66+
.field("port", &self.port)
67+
.field("username", &self.username)
68+
.field("password", &"[redacted]")
69+
.field("database", &self.database)
70+
.field(
71+
"allow_statement_executions_against",
72+
&self.allow_statement_executions_against,
73+
)
74+
.field("conn_timeout_secs", &self.conn_timeout_secs)
75+
.field("disable_connection", &self.disable_connection)
76+
.finish()
77+
}
78+
}
79+
5280
impl Default for DatabaseConfiguration {
5381
fn default() -> Self {
5482
Self {
@@ -64,3 +92,39 @@ impl Default for DatabaseConfiguration {
6492
}
6593
}
6694
}
95+
96+
impl PartialDatabaseConfiguration {
97+
/// Creates a partial configuration from standard Postgres environment variables.
98+
///
99+
/// Reads `DATABASE_URL`, `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, and `PGDATABASE`.
100+
/// Returns `None` if no relevant env vars are set.
101+
pub fn from_env() -> Option<Self> {
102+
let database_url = std::env::var("DATABASE_URL").ok();
103+
let pghost = std::env::var("PGHOST").ok();
104+
let pgport = std::env::var("PGPORT").ok().and_then(|p| p.parse().ok());
105+
let pguser = std::env::var("PGUSER").ok();
106+
let pgpassword = std::env::var("PGPASSWORD").ok();
107+
let pgdatabase = std::env::var("PGDATABASE").ok();
108+
109+
let has_any = database_url.is_some()
110+
|| pghost.is_some()
111+
|| pgport.is_some()
112+
|| pguser.is_some()
113+
|| pgpassword.is_some()
114+
|| pgdatabase.is_some();
115+
116+
if !has_any {
117+
return None;
118+
}
119+
120+
Some(Self {
121+
connection_string: database_url,
122+
host: pghost,
123+
port: pgport,
124+
username: pguser,
125+
password: pgpassword,
126+
database: pgdatabase,
127+
..Default::default()
128+
})
129+
}
130+
}

crates/pgls_env/src/lib.rs

Lines changed: 49 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,22 @@ 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("PGHOST", "The host of the database server."),
80+
pgport: PgLSEnvVariable::new("PGPORT", "The port of the database server."),
81+
pguser: PgLSEnvVariable::new("PGUSER", "The username to connect to the database."),
82+
pgpassword: PgLSEnvVariable::new(
83+
"PGPASSWORD",
84+
"The password to connect to the database.",
85+
),
86+
pgdatabase: PgLSEnvVariable::new(
87+
"PGDATABASE",
88+
"The name of the database to connect to.",
89+
),
90+
6791
pgt_log_path: PgLSEnvVariable::new(
6892
"PGT_LOG_PATH",
6993
"The directory where the Daemon logs will be saved. Deprecated, use PGLS_LOG_PATH instead.",
@@ -156,6 +180,31 @@ impl Display for PgLSEnv {
156180
}
157181
};
158182

183+
let sensitive = [&self.database_url, &self.pgpassword];
184+
let non_sensitive = [&self.pghost, &self.pgport, &self.pguser, &self.pgdatabase];
185+
186+
for var in sensitive {
187+
match var.value() {
188+
None => {
189+
KeyValuePair(var.name, markup! { <Dim>"unset"</Dim> }).fmt(fmt)?;
190+
}
191+
Some(_) => {
192+
KeyValuePair(var.name, markup! { <Dim>"set"</Dim> }).fmt(fmt)?;
193+
}
194+
};
195+
}
196+
197+
for var in non_sensitive {
198+
match var.value() {
199+
None => {
200+
KeyValuePair(var.name, markup! { <Dim>"unset"</Dim> }).fmt(fmt)?;
201+
}
202+
Some(value) => {
203+
KeyValuePair(var.name, markup! {{DebugDisplay(value)}}).fmt(fmt)?;
204+
}
205+
};
206+
}
207+
159208
match self.pgt_log_path.value() {
160209
None => {
161210
KeyValuePair(self.pgt_log_path.name, markup! { <Dim>"unset"</Dim> }).fmt(fmt)?;

crates/pgls_lsp/src/server.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::session::{
66
use crate::utils::{into_lsp_error, panic_to_lsp_error};
77
use futures::FutureExt;
88
use futures::future::ready;
9+
use pgls_configuration::database::PartialDatabaseConfiguration;
910
use pgls_fs::{ConfigName, FileSystem, OsFileSystem};
1011
use pgls_workspace::workspace::{RegisterProjectFolderParams, UnregisterProjectFolderParams};
1112
use pgls_workspace::{DynRef, Workspace, workspace};
@@ -162,9 +163,9 @@ impl LanguageServer for LSPServer {
162163

163164
#[tracing::instrument(level = "info", skip_all)]
164165
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
165-
self.session
166-
.load_workspace_settings(serde_json::from_value(params.settings).ok())
167-
.await;
166+
let extra_config: Option<pgls_configuration::PartialConfiguration> =
167+
serde_json::from_value(params.settings).ok();
168+
self.session.load_workspace_settings(extra_config).await;
168169
self.setup_capabilities().await;
169170
self.session.update_all_diagnostics().await;
170171
}
@@ -410,17 +411,27 @@ pub struct ServerFactory {
410411
/// This shared flag is set to true once at least one sessions has been
411412
/// initialized on this server instance
412413
is_initialized: Arc<AtomicBool>,
414+
/// Configuration from environment variables (DATABASE_URL, PGHOST, etc.).
415+
/// Computed once at factory creation and passed to each session.
416+
env_config: Option<pgls_configuration::PartialConfiguration>,
413417
}
414418

415419
impl ServerFactory {
416420
pub fn new(stop_on_disconnect: bool) -> Self {
421+
let env_config = PartialDatabaseConfiguration::from_env().map(|db| {
422+
pgls_configuration::PartialConfiguration {
423+
db: Some(db),
424+
..Default::default()
425+
}
426+
});
417427
Self {
418428
cancellation: Arc::default(),
419429
workspace: None,
420430
sessions: Sessions::default(),
421431
next_session_key: AtomicU64::new(0),
422432
stop_on_disconnect,
423433
is_initialized: Arc::default(),
434+
env_config,
424435
}
425436
}
426437

@@ -441,13 +452,15 @@ impl ServerFactory {
441452

442453
let session_key = SessionKey(self.next_session_key.fetch_add(1, Ordering::Relaxed));
443454

455+
let env_config = self.env_config.clone();
444456
let mut builder = LspService::build(move |client| {
445457
let mut session = Session::new(
446458
session_key,
447459
client,
448460
workspace,
449461
self.cancellation.clone(),
450462
fs,
463+
env_config,
451464
);
452465
if let Some(path) = config_path {
453466
session.set_config_path(path);

0 commit comments

Comments
 (0)