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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- hosted behind an ssl-terminating reverse proxy
- New docker image variant: `lovasoa/sqlpage:latest-duckdb`, `lovasoa/sqlpage:main-duckdb` with preconfigured duckdb odbc drivers.
- New config option: `cache_stale_duration_ms` to control the duration for which cached sql files are considered fresh.
- Add support for standard PostgreSQL environment variables (`PGHOST`, `PGUSER`, `PGPASSWORD`, etc.) to configure the database connection.

## 0.41.0 (2025-12-28)

Expand Down
53 changes: 22 additions & 31 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ If the `database_password` configuration parameter is set, it will override any
It does not need to be percent-encoded.
This allows you to keep the password separate from the connection string, which can be useful for security purposes, especially when storing configurations in version control systems.

Standard [PostgreSQL environment variables](https://www.postgresql.org/docs/current/libpq-envars.html) are also supported.

### OpenID Connect (OIDC) Authentication

OpenID Connect (OIDC) is a secure way to let users log in to your SQLPage application using their existing accounts from popular services. When OIDC is configured, you can control which parts of your application require authentication using the `oidc_protected_paths` option. By default, all pages are protected. You can specify a list of URL prefixes to protect specific areas, allowing you to have a mix of public and private pages.
Expand Down
57 changes: 52 additions & 5 deletions src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,18 @@ impl AppConfig {
}

if config.database_url.is_empty() {
log::debug!(
"Creating default database in {}",
config.configuration_directory.display()
);
config.database_url = create_default_database(&config.configuration_directory);
// If the database URL is empty, we check if the user has set the PGDATABASE environment variable.
// If they have, we assume they want to use the postgres environment variables to connect to the database.
// We set the database URL to a special value that will be recognized by the database connection code.
if std::env::var("PGDATABASE").is_ok() || std::env::var("PGHOST").is_ok() {
config.database_url = "postgres://:env:".to_string();
} else {
log::debug!(
"Creating default database in {}",
config.configuration_directory.display()
);
config.database_url = create_default_database(&config.configuration_directory);
}
}

config
Expand Down Expand Up @@ -785,6 +792,46 @@ mod test {
std::fs::remove_dir_all(&temp_dir).unwrap();
}

#[test]
fn test_postgres_env_vars() {
let _lock = ENV_LOCK
.lock()
.expect("Another test panicked while holding the lock");

// Test 1: No config, only env vars
env::set_var("PGHOST", "localhost");
env::set_var("PGUSER", "myuser");
env::set_var("PGDATABASE", "mydb");
env::set_var("PGPASSWORD", "mypass");

// We need to clear other env vars that might interfere
env::remove_var("SQLPAGE_DATABASE_URL");
env::remove_var("DATABASE_URL");

let cli = Cli {
web_root: None,
config_dir: None,
config_file: None,
command: None,
};

let config = AppConfig::from_cli(&cli).unwrap();

assert_eq!(config.database_url, "postgres://:env:");

// Test 2: Config overrides env vars
env::set_var("SQLPAGE_DATABASE_URL", "sqlite://:memory:");
let config = AppConfig::from_cli(&cli).unwrap();
assert_eq!(config.database_url, "sqlite://:memory:");

// Cleanup
env::remove_var("PGHOST");
env::remove_var("PGUSER");
env::remove_var("PGDATABASE");
env::remove_var("PGPASSWORD");
env::remove_var("SQLPAGE_DATABASE_URL");
}

#[test]
fn test_default_values() {
let _lock = ENV_LOCK
Expand Down
47 changes: 42 additions & 5 deletions src/webserver/database/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
use anyhow::Context;
use futures_util::future::BoxFuture;
use sqlx::odbc::OdbcConnectOptions;
use sqlx::postgres::PgConnectOptions;
use sqlx::{
any::{Any, AnyConnectOptions, AnyKind},
pool::PoolOptions,
Expand All @@ -19,9 +20,21 @@ use sqlx::{
impl Database {
pub async fn init(config: &AppConfig) -> anyhow::Result<Self> {
let database_url = &config.database_url;
let mut connect_options: AnyConnectOptions = database_url
.parse()
.with_context(|| format!("\"{database_url}\" is not a valid database URL. Please change the \"database_url\" option in the configuration file."))?;
let mut connect_options: AnyConnectOptions = if database_url == "postgres://:env:" {
PgConnectOptions::new().into()
} else {
let mut options: AnyConnectOptions = database_url
.parse()
.with_context(|| format!("\"{database_url}\" is not a valid database URL. Please change the \"database_url\" option in the configuration file."))?;
if options.kind() == AnyKind::Postgres
&& !url_has_password(database_url)
{
if let Ok(password) = std::env::var("PGPASSWORD") {
set_database_password(&mut options, &password);
}
}
options
};
if let Some(password) = &config.database_password {
set_database_password(&mut connect_options, password);
}
Expand Down Expand Up @@ -77,8 +90,8 @@ impl Database {
} else {
// Different databases have a different number of max concurrent connections allowed by default
match kind {
AnyKind::Postgres | AnyKind::Odbc => 50, // Default to PostgreSQL-like limits for Generic
AnyKind::MySql => 75,
// AnyKind::Postgres => 50, // Default to PostgreSQL-like limits for Generic
// AnyKind::MySql => 75,
AnyKind::Sqlite => {
if config.database_url.contains(":memory:") {
128
Expand Down Expand Up @@ -249,6 +262,16 @@ fn set_custom_connect_options_odbc(odbc_options: &mut OdbcConnectOptions, config
odbc_options.max_column_size(None);
}

fn url_has_password(url: &str) -> bool {
if let Some(rest) = url.strip_prefix("postgres://").or_else(|| url.strip_prefix("postgresql://")) {
if let Some(at_index) = rest.find('@') {
let user_info = &rest[..at_index];
return user_info.contains(':');
}
}
false
}

fn set_database_password(options: &mut AnyConnectOptions, password: &str) {
if let Some(opts) = options.as_postgres_mut() {
*opts = take(opts).password(password);
Expand All @@ -266,3 +289,17 @@ fn set_database_password(options: &mut AnyConnectOptions, password: &str) {
unreachable!("Unsupported database type");
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_url_has_password() {
assert!(url_has_password("postgres://user:pass@host/db"));
assert!(url_has_password("postgresql://user:pass@host/db"));
assert!(!url_has_password("postgres://user@host/db"));
assert!(!url_has_password("postgres://host/db"));
assert!(!url_has_password("mysql://user:pass@host/db"));
}
}
Loading