Skip to content

Commit 43cb27a

Browse files
committed
fix(macros): smarter .env loading, caching, and invalidation
1 parent 064d649 commit 43cb27a

File tree

14 files changed

+449
-279
lines changed

14 files changed

+449
-279
lines changed

Cargo.lock

Lines changed: 30 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ uuid = "1.1.2"
190190
# Common utility crates
191191
cfg-if = "1.0.0"
192192
dotenvy = { version = "0.15.0", default-features = false }
193+
thiserror = { version = "2.0.17", default-features = false, features = ["std"] }
193194

194195
# Runtimes
195196
[workspace.dependencies.async-global-executor]

sqlx-core/Cargo.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,22 @@ rustls-native-certs = { version = "0.8.0", optional = true }
6262
# Type Integrations
6363
bit-vec = { workspace = true, optional = true }
6464
bigdecimal = { workspace = true, optional = true }
65+
chrono = { workspace = true, optional = true }
6566
rust_decimal = { workspace = true, optional = true }
6667
time = { workspace = true, optional = true }
6768
ipnet = { workspace = true, optional = true }
6869
ipnetwork = { workspace = true, optional = true }
6970
mac_address = { workspace = true, optional = true }
7071
uuid = { workspace = true, optional = true }
7172

73+
# work around bug in async-fs 2.0.0, which references futures-lite dependency wrongly, see https://github.com/launchbadge/sqlx/pull/3791#issuecomment-3043363281
74+
async-fs = { version = "2.1", optional = true }
7275
async-io = { version = "2.4.1", optional = true }
7376
async-task = { version = "4.7.1", optional = true }
7477

75-
# work around bug in async-fs 2.0.0, which references futures-lite dependency wrongly, see https://github.com/launchbadge/sqlx/pull/3791#issuecomment-3043363281
76-
async-fs = { version = "2.1", optional = true }
7778
base64 = { version = "0.22.0", default-features = false, features = ["std"] }
7879
bytes = "1.1.0"
7980
cfg-if = { workspace = true }
80-
chrono = { version = "0.4.34", default-features = false, features = ["clock"], optional = true }
8181
crc = { version = "3", optional = true }
8282
crossbeam-queue = "0.3.2"
8383
either = "1.6.1"
@@ -93,7 +93,6 @@ serde_json = { version = "1.0.73", features = ["raw_value"], optional = true }
9393
toml = { version = "0.8.16", optional = true }
9494
sha2 = { version = "0.10.0", default-features = false, optional = true }
9595
#sqlformat = "0.2.0"
96-
thiserror = "2.0.0"
9796
tokio-stream = { version = "0.1.8", features = ["fs"], optional = true }
9897
tracing = { version = "0.1.37", features = ["log"] }
9998
smallvec = "1.7.0"
@@ -102,7 +101,9 @@ bstr = { version = "1.0", default-features = false, features = ["std"], optional
102101
hashlink = "0.10.0"
103102
indexmap = "2.0"
104103
event-listener = "5.2.0"
105-
hashbrown = "0.15.0"
104+
hashbrown = "0.16.0"
105+
106+
thiserror.workspace = true
106107

107108
[dev-dependencies]
108109
sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] }

sqlx-core/src/config/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,16 @@ impl Config {
158158
/// * If the file exists but could not be read or parsed.
159159
/// * If the file exists but the `sqlx-toml` feature is disabled.
160160
pub fn try_from_crate_or_default() -> Result<Self, ConfigError> {
161-
Self::read_from(get_crate_path()?).or_else(|e| {
161+
Self::try_from_path_or_default(get_crate_path()?)
162+
}
163+
164+
/// Attempt to read `Config` from the path given, or return `Config::default()` if it does not exist.
165+
///
166+
/// # Errors
167+
/// * If the file exists but could not be read or parsed.
168+
/// * If the file exists but the `sqlx-toml` feature is disabled.
169+
pub fn try_from_path_or_default(path: PathBuf) -> Result<Self, ConfigError> {
170+
Self::read_from(path).or_else(|e| {
162171
if let ConfigError::NotFound { .. } = e {
163172
Ok(Config::default())
164173
} else {

sqlx-macros-core/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ _sqlite = []
2626

2727
# SQLx features
2828
derive = []
29-
macros = []
29+
macros = ["thiserror"]
3030
migrate = ["sqlx-core/migrate"]
3131

3232
sqlx-toml = ["sqlx-core/sqlx-toml", "sqlx-sqlite?/sqlx-toml"]
@@ -66,6 +66,7 @@ tokio = { workspace = true, optional = true }
6666

6767
cfg-if = { workspace = true}
6868
dotenvy = { workspace = true }
69+
thiserror = { workspace = true, optional = true }
6970

7071
hex = { version = "0.4.3" }
7172
heck = { version = "0.5" }

sqlx-macros-core/clippy.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[disallowed-methods]]
2+
path = "std::env::var"
3+
reason = "use `crate::env()` instead, which optionally calls `proc_macro::tracked_env::var()`"

sqlx-macros-core/src/lib.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::query::QueryDriver;
2626

2727
pub type Error = Box<dyn std::error::Error>;
2828

29-
pub type Result<T> = std::result::Result<T, Error>;
29+
pub type Result<T, E = Error> = std::result::Result<T, E>;
3030

3131
mod common;
3232
pub mod database;
@@ -84,3 +84,24 @@ where
8484
}
8585
}
8686
}
87+
88+
pub fn env(var: &str) -> Result<String> {
89+
env_opt(var)?
90+
.ok_or_else(|| format!("env var {var:?} must be set to use the query macros").into())
91+
}
92+
93+
pub fn env_opt(var: &str) -> Result<Option<String>> {
94+
use std::env::VarError;
95+
96+
#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
97+
let res: Result<String, VarError> = proc_macro::tracked_env::var(var);
98+
99+
#[cfg(not(any(sqlx_macros_unstable, procmacro2_semver_exempt)))]
100+
let res: Result<String, VarError> = std::env::var(var);
101+
102+
match res {
103+
Ok(val) => Ok(Some(val)),
104+
Err(VarError::NotPresent) => Ok(None),
105+
Err(VarError::NotUnicode(_)) => Err(format!("env var {var:?} is not valid UTF-8").into()),
106+
}
107+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use std::path::{Path, PathBuf};
2+
use std::sync::Mutex;
3+
use std::time::SystemTime;
4+
5+
/// A cached value derived from one or more files, which is automatically invalidated
6+
/// if the modified-time of any watched file changes.
7+
pub struct MtimeCache<T> {
8+
inner: Mutex<Option<MtimeCacheInner<T>>>,
9+
}
10+
11+
pub struct MtimeCacheBuilder {
12+
file_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
13+
}
14+
15+
struct MtimeCacheInner<T> {
16+
builder: MtimeCacheBuilder,
17+
cached: T,
18+
}
19+
20+
impl<T: Clone> MtimeCache<T> {
21+
pub fn new() -> Self {
22+
MtimeCache {
23+
inner: Mutex::new(None),
24+
}
25+
}
26+
27+
/// Get the cached value, or (re)initialize it if it does not exist or a file's mtime has changed.
28+
pub fn get_or_try_init<E>(
29+
&self,
30+
init: impl FnOnce(&mut MtimeCacheBuilder) -> Result<T, E>,
31+
) -> Result<T, E> {
32+
let mut inner = self.inner.lock().unwrap_or_else(|e| {
33+
// Reset the cache on-panic.
34+
let mut locked = e.into_inner();
35+
*locked = None;
36+
locked
37+
});
38+
39+
if let Some(inner) = &*inner {
40+
if !inner.builder.any_modified() {
41+
return Ok(inner.cached.clone());
42+
}
43+
}
44+
45+
let mut builder = MtimeCacheBuilder::new();
46+
47+
let value = init(&mut builder)?;
48+
49+
*inner = Some(MtimeCacheInner {
50+
builder,
51+
cached: value.clone(),
52+
});
53+
54+
Ok(value)
55+
}
56+
}
57+
58+
impl MtimeCacheBuilder {
59+
fn new() -> Self {
60+
MtimeCacheBuilder {
61+
file_mtimes: Vec::new(),
62+
}
63+
}
64+
65+
/// Add a file path to watch.
66+
///
67+
/// The cached value will be automatically invalidated if the modified-time of the file changes,
68+
/// or if the file does not exist but is created sometime after this call.
69+
pub fn add_path(&mut self, path: PathBuf) {
70+
let mtime = get_mtime(&path);
71+
72+
#[cfg(any(sqlx_macros_unstable, procmacro2_semver_exempt))]
73+
{
74+
proc_macro::tracked_path::path(&path);
75+
}
76+
77+
self.file_mtimes.push((path, mtime));
78+
}
79+
80+
fn any_modified(&self) -> bool {
81+
for (path, expected_mtime) in &self.file_mtimes {
82+
let actual_mtime = get_mtime(path);
83+
84+
if expected_mtime != &actual_mtime {
85+
return true;
86+
}
87+
}
88+
89+
false
90+
}
91+
}
92+
93+
fn get_mtime(path: &Path) -> Option<SystemTime> {
94+
std::fs::metadata(path)
95+
.and_then(|metadata| metadata.modified())
96+
.ok()
97+
}

0 commit comments

Comments
 (0)