1use ecat_data::Dialect;
4use ecat_tls::TlsClientConfig;
5use serde::Deserialize;
6use std::time::Duration;
7
8#[derive(Debug, Clone, Deserialize)]
9pub struct SqlxConfig {
10 pub url: String,
11 #[serde(default)]
12 pub username: Option<String>,
13 #[serde(default)]
14 pub password: Option<String>,
15 #[serde(default)]
19 pub tls: Option<TlsClientConfig>,
20 #[serde(default)]
23 pub session_init: Option<Vec<String>>,
24
25 #[serde(default)]
26 pub max_connections: Option<u32>,
27 #[serde(default)]
28 pub min_connections: Option<u32>,
29 #[serde(default)]
33 pub acquire_timeout_secs: Option<u64>,
34 #[serde(default)]
35 pub idle_timeout_secs: Option<u64>,
36 #[serde(default)]
37 pub max_lifetime_secs: Option<u64>,
38 #[serde(default)]
40 pub query_timeout_secs: Option<u64>,
41 #[serde(default)]
45 pub test_before_acquire: Option<bool>,
46}
47
48#[derive(Debug, Clone)]
50pub struct PoolParams {
51 pub max_connections: u32,
52 pub min_connections: u32,
53 pub acquire_timeout: Duration,
54 pub idle_timeout: Duration,
55 pub max_lifetime: Duration,
56 pub query_timeout: Option<Duration>,
57 pub test_before_acquire: bool,
58 pub session_init: Vec<String>,
60}
61
62impl Default for PoolParams {
63 fn default() -> Self {
66 Self {
67 max_connections: 10,
68 min_connections: 0,
69 acquire_timeout: Duration::from_secs(30),
70 idle_timeout: Duration::from_secs(600),
71 max_lifetime: Duration::from_secs(1800),
72 query_timeout: Some(Duration::from_secs(30)),
73 test_before_acquire: false,
74 session_init: Vec::new(),
75 }
76 }
77}
78
79impl PoolParams {
80 pub fn for_url(url: &str) -> Self {
86 Self {
87 session_init: dialect_session_init(url),
88 ..Self::default()
89 }
90 }
91}
92
93pub fn dialect_session_init(url: &str) -> Vec<String> {
96 match Dialect::from_url(url) {
97 Dialect::Postgres => vec![
98 "SET TIME ZONE 'UTC'".to_string(),
99 "SET application_name = 'ecat'".to_string(),
100 ],
101 Dialect::MySql => vec!["SET time_zone = '+00:00'".to_string()],
102 _ => Vec::new(),
103 }
104}
105
106impl SqlxConfig {
107 pub fn pool(&self) -> PoolParams {
110 let d = PoolParams::default();
111 let max_connections = self.max_connections.unwrap_or(d.max_connections);
112 PoolParams {
113 max_connections,
114 min_connections: self
119 .min_connections
120 .unwrap_or(d.min_connections)
121 .min(max_connections),
122 acquire_timeout: self
123 .acquire_timeout_secs
124 .map_or(d.acquire_timeout, Duration::from_secs),
125 idle_timeout: self
126 .idle_timeout_secs
127 .map_or(d.idle_timeout, Duration::from_secs),
128 max_lifetime: self
129 .max_lifetime_secs
130 .map_or(d.max_lifetime, Duration::from_secs),
131 query_timeout: self.query_timeout(),
132 test_before_acquire: self.test_before_acquire.unwrap_or(d.test_before_acquire),
133 session_init: self.effective_session_init(),
134 }
135 }
136
137 pub fn query_timeout(&self) -> Option<Duration> {
139 match self.query_timeout_secs {
140 None => Some(Duration::from_secs(30)),
141 Some(0) => None,
142 Some(s) => Some(Duration::from_secs(s)),
143 }
144 }
145
146 pub fn effective_session_init(&self) -> Vec<String> {
152 self.session_init
153 .clone()
154 .unwrap_or_else(|| dialect_session_init(&self.url))
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn minimal_config_uses_documented_defaults() {
164 let cfg: SqlxConfig =
165 serde_json::from_str(r#"{"url": "postgres://localhost/db"}"#).unwrap();
166 let p = cfg.pool();
167 assert_eq!(p.max_connections, 10);
168 assert_eq!(p.min_connections, 0);
169 assert_eq!(p.acquire_timeout, Duration::from_secs(30));
170 assert_eq!(p.idle_timeout, Duration::from_secs(600));
171 assert_eq!(p.max_lifetime, Duration::from_secs(1800));
172 assert_eq!(p.query_timeout, Some(Duration::from_secs(30)));
173 assert!(!p.test_before_acquire);
174 assert!(cfg.session_init.is_none());
175 }
176
177 #[test]
178 fn pool_params_can_be_overridden() {
179 let cfg: SqlxConfig = serde_json::from_str(
180 r#"{"url": "mysql://localhost/db",
181 "max_connections": 32,
182 "query_timeout_secs": 0,
183 "test_before_acquire": true,
184 "session_init": ["SET time_zone = '+00:00'"]}"#,
185 )
186 .unwrap();
187 assert_eq!(cfg.pool().max_connections, 32);
188 assert_eq!(cfg.pool().query_timeout, None);
189 assert!(cfg.pool().test_before_acquire);
190 assert_eq!(
191 cfg.effective_session_init(),
192 vec!["SET time_zone = '+00:00'".to_string()]
193 );
194 }
195
196 #[test]
199 fn session_init_defaults_per_dialect() {
200 let pg: SqlxConfig = serde_json::from_str(r#"{"url": "postgres://h/db"}"#).unwrap();
201 assert!(
202 pg.effective_session_init()
203 .iter()
204 .any(|s| s.contains("TIME ZONE"))
205 );
206
207 let sqlite: SqlxConfig = serde_json::from_str(r#"{"url": "sqlite::memory:"}"#).unwrap();
208 assert!(sqlite.effective_session_init().is_empty());
209 }
210
211 #[test]
214 fn for_url_matches_pool_defaults_field_by_field() {
215 for url in ["postgres://h/db", "mysql://h/db", "sqlite::memory:"] {
216 let cfg: SqlxConfig = serde_json::from_str(&format!(r#"{{"url": "{url}"}}"#)).unwrap();
217 let a = PoolParams::for_url(url);
218 let b = cfg.pool();
219 assert_eq!(a.max_connections, b.max_connections, "{url}");
220 assert_eq!(a.min_connections, b.min_connections, "{url}");
221 assert_eq!(a.acquire_timeout, b.acquire_timeout, "{url}");
222 assert_eq!(a.idle_timeout, b.idle_timeout, "{url}");
223 assert_eq!(a.max_lifetime, b.max_lifetime, "{url}");
224 assert_eq!(a.query_timeout, b.query_timeout, "{url}");
225 assert_eq!(a.test_before_acquire, b.test_before_acquire, "{url}");
226 assert_eq!(a.session_init, b.session_init, "{url}");
227 }
228 }
229
230 #[test]
232 fn min_connections_is_clamped_to_max() {
233 let cfg: SqlxConfig = serde_json::from_str(
234 r#"{"url": "sqlite::memory:", "max_connections": 2, "min_connections": 5}"#,
235 )
236 .unwrap();
237 let p = cfg.pool();
238 assert_eq!(p.max_connections, 2);
239 assert_eq!(p.min_connections, 2);
240 }
241
242 #[test]
244 fn zero_query_timeout_means_disabled() {
245 let cfg: SqlxConfig =
246 serde_json::from_str(r#"{"url": "sqlite::memory:", "query_timeout_secs": 0}"#).unwrap();
247 assert_eq!(cfg.query_timeout(), None);
248 }
249
250 #[tokio::test]
252 async fn tls_field_is_rejected_not_silently_ignored() {
253 let cfg: SqlxConfig =
254 serde_json::from_str(r#"{"url": "postgres://h/db", "tls": {"skip_verify": true}}"#)
255 .unwrap();
256 assert!(
257 cfg.tls.is_some(),
258 "serde 应当照旧接受该字段(报错发生在 from_config)"
259 );
260
261 let Err(err) = crate::SqlxClient::from_config(cfg).await else {
263 panic!("tls 字段设了必须报错,不能静默忽略");
264 };
265 let msg = err.to_string();
266 assert!(msg.contains("tls"), "got: {msg}");
267 assert!(
268 msg.contains("sslmode"),
269 "错误信息要指向正确做法,got: {msg}"
270 );
271 }
272}