Skip to content

Commit a8a9e0c

Browse files
committed
fix(macros-core): more consistent env loading and reading logic
While giving the new `sqlx.toml` feature a try, I discovered that its `database_url_var` setting behaved inconsistently compared to other environment variables: it wasn't loaded from `.env` files, causing it to be effectively ignored in favor of the default `DATABASE_URL` variable. This is an inconvenient outcome for the multi-database workspaces that this feature is designed to support. To address this issue, I reworked the `.env` loading logic to account for this configuration and make it simpler to use to achieve consistent behavior. The new algorithm works as follows: - First, it generates lists of environment variables that can be loaded from `.env` files and candidate `.env` file paths. When applicable, the compiler is informed to track changes to their elements. - Next, it loads the set of potentially tracked environment variables using `set_var`, similar to how `dotenvy::dotenv()` operates. When a variable is defined in both the process environment and a `.env` file, the process environment takes precedence, as before. - Macro code can now use the `env` function freely to read environment variable values, abstracting itself away from their source, which results in simpler, less error-prone code. Trivially, this rework resolves the issue I encountered because the `database_url_var` value is now part of the list of loadable environment variables. Future code can easily make such additions as necessary.
1 parent 69bb595 commit a8a9e0c

File tree

1 file changed

+83
-48
lines changed
  • sqlx-macros-core/src/query

1 file changed

+83
-48
lines changed

sqlx-macros-core/src/query/mod.rs

Lines changed: 83 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#[cfg(procmacro2_semver_exempt)]
2+
use std::collections::HashSet;
13
use std::collections::{hash_map, HashMap};
4+
#[cfg(procmacro2_semver_exempt)]
5+
use std::hash::{BuildHasherDefault, DefaultHasher};
26
use std::path::{Path, PathBuf};
37
use std::sync::{Arc, LazyLock, Mutex};
48
use std::{fs, io};
@@ -115,20 +119,19 @@ static METADATA: LazyLock<Mutex<HashMap<String, Metadata>>> = LazyLock::new(Defa
115119
// reflect the workspace dir: https://github.com/rust-lang/cargo/issues/3946
116120
fn init_metadata(manifest_dir: &String) -> crate::Result<Metadata> {
117121
let manifest_dir: PathBuf = manifest_dir.into();
122+
let config = Config::try_from_crate_or_default()?;
118123

119-
let (database_url, offline, offline_dir) = load_dot_env(&manifest_dir);
124+
load_env(&manifest_dir, &config);
120125

121126
let offline = env("SQLX_OFFLINE")
122-
.ok()
123-
.or(offline)
124127
.map(|s| s.eq_ignore_ascii_case("true") || s == "1")
125128
.unwrap_or(false);
126129

127-
let offline_dir = env("SQLX_OFFLINE_DIR").ok().or(offline_dir);
130+
let offline_dir = env("SQLX_OFFLINE_DIR").ok();
128131

129-
let config = Config::try_from_crate_or_default()?;
130-
131-
let database_url = env(config.common.database_url_var()).ok().or(database_url);
132+
let database_url = env(config.common.database_url_var())
133+
.ok()
134+
.or_else(|| env("DATABASE_URL").ok());
132135

133136
Ok(Metadata {
134137
manifest_dir,
@@ -415,64 +418,96 @@ where
415418
Ok(ret_tokens)
416419
}
417420

421+
#[cfg(procmacro2_semver_exempt)]
422+
static TRACKED_ENV_VARS: Mutex<HashSet<String, BuildHasherDefault<DefaultHasher>>> =
423+
Mutex::new(HashSet::with_hasher(BuildHasherDefault::new()));
424+
418425
/// Get the value of an environment variable, telling the compiler about it if applicable.
419426
fn env(name: &str) -> Result<String, std::env::VarError> {
420427
#[cfg(procmacro2_semver_exempt)]
421-
{
428+
if TRACKED_ENV_VARS.lock().unwrap().insert(name.to_string()) {
429+
// Avoid tracking the same env var multiple times, which would undesirably modify
430+
// build system state and thus behavior in case we change var values.
422431
proc_macro::tracked_env::var(name)
432+
} else {
433+
std::env::var(name)
423434
}
424435

425436
#[cfg(not(procmacro2_semver_exempt))]
426-
{
427-
std::env::var(name)
428-
}
437+
std::env::var(name)
429438
}
430439

431-
/// Get `DATABASE_URL`, `SQLX_OFFLINE` and `SQLX_OFFLINE_DIR` from the `.env`.
432-
fn load_dot_env(manifest_dir: &Path) -> (Option<String>, Option<String>, Option<String>) {
433-
let mut env_path = manifest_dir.join(".env");
434-
435-
// If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this,
436-
// otherwise fallback to default dotenv file.
437-
#[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))]
438-
let env_file = if env_path.exists() {
439-
let res = dotenvy::from_path_iter(&env_path);
440-
match res {
441-
Ok(iter) => Some(iter),
442-
Err(e) => panic!("failed to load environment from {env_path:?}, {e}"),
443-
}
444-
} else {
445-
#[allow(unused_assignments)]
446-
{
447-
env_path = PathBuf::from(".env");
440+
/// Load configuration environment variables from a `.env` file, without overriding existing
441+
/// environment variables. If applicable, the compiler is informed about the loaded env vars
442+
/// and the `.env` files they may come from.
443+
fn load_env(manifest_dir: &Path, config: &Config) {
444+
let loadable_vars = [
445+
"DATABASE_URL",
446+
"SQLX_OFFLINE",
447+
"SQLX_OFFLINE_DIR",
448+
config.common.database_url_var(),
449+
];
450+
451+
let (found_dotenv, candidate_dotenv_paths) = find_dotenv(&manifest_dir);
452+
453+
// Tell the compiler to watch the candidate `.env` paths for changes. It's important to
454+
// watch them all, because there are several possible locations where a `.env` file
455+
// might be read, and we want to react to changes in any of them.
456+
#[cfg(procmacro2_semver_exempt)]
457+
for path in &candidate_dotenv_paths {
458+
if let Some(path) = path.to_str() {
459+
proc_macro::tracked_path::path(path);
448460
}
449-
dotenvy::dotenv_iter().ok()
450-
};
451-
452-
let mut offline = None;
453-
let mut database_url = None;
454-
let mut offline_dir = None;
461+
}
455462

456-
if let Some(env_file) = env_file {
457-
// tell the compiler to watch the `.env` for changes.
458-
#[cfg(procmacro2_semver_exempt)]
459-
if let Some(env_path) = env_path.to_str() {
460-
proc_macro::tracked_path::path(env_path);
463+
// Tell the compiler about the environment variables we care about before we load them
464+
// from any `.env`, so the build system can react to changes in their original values,
465+
// not the values we load from a potential `.env` file tracked above, which should not
466+
// take precedence.
467+
#[cfg(procmacro2_semver_exempt)]
468+
for name in &loadable_vars {
469+
if TRACKED_ENV_VARS.lock().unwrap().insert(name.to_string()) {
470+
proc_macro::tracked_env::var(name);
461471
}
472+
}
462473

463-
for item in env_file {
464-
let Ok((key, value)) = item else {
474+
if let Some(dotenv_path) = found_dotenv
475+
.then_some(candidate_dotenv_paths)
476+
.iter()
477+
.flatten()
478+
.last()
479+
{
480+
for dotenv_var_result in dotenvy::from_path_iter(dotenv_path)
481+
.ok()
482+
.into_iter()
483+
.flatten()
484+
{
485+
let Ok((key, value)) = dotenv_var_result else {
465486
continue;
466487
};
467488

468-
match key.as_str() {
469-
"DATABASE_URL" => database_url = Some(value),
470-
"SQLX_OFFLINE" => offline = Some(value),
471-
"SQLX_OFFLINE_DIR" => offline_dir = Some(value),
472-
_ => {}
473-
};
489+
if loadable_vars.contains(&&*key) && std::env::var(&key).is_err() {
490+
std::env::set_var(key, value);
491+
}
474492
}
475493
}
494+
}
495+
496+
fn find_dotenv(mut dir: &Path) -> (bool, Vec<PathBuf>) {
497+
let mut candidate_files = vec![];
498+
499+
loop {
500+
candidate_files.push(dir.join(".env"));
501+
let candidate_file = candidate_files.last().unwrap();
476502

477-
(database_url, offline, offline_dir)
503+
if candidate_file.is_file() {
504+
return (true, candidate_files);
505+
}
506+
507+
if let Some(parent) = dir.parent() {
508+
dir = parent;
509+
} else {
510+
return (false, candidate_files);
511+
}
512+
}
478513
}

0 commit comments

Comments
 (0)