1use ecat_data::Dialect;
4use ecat_tls::TlsClientConfig;
5use serde::Deserialize;
6use std::time::Duration;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum MysqlCompat {
17 #[default]
19 Mysql,
20 Doris,
22 StarRocks,
24}
25
26#[derive(Debug, Clone, Deserialize)]
27pub struct SqlxConfig {
28 pub url: String,
29 #[serde(default)]
30 pub username: Option<String>,
31 #[serde(default)]
32 pub password: Option<String>,
33 #[serde(default)]
37 pub tls: Option<TlsClientConfig>,
38 #[serde(default)]
41 pub session_init: Option<Vec<String>>,
42 #[serde(default)]
47 pub mysql_compat: Option<MysqlCompat>,
48
49 #[serde(default)]
50 pub max_connections: Option<u32>,
51 #[serde(default)]
52 pub min_connections: Option<u32>,
53 #[serde(default)]
57 pub acquire_timeout_secs: Option<u64>,
58 #[serde(default)]
59 pub idle_timeout_secs: Option<u64>,
60 #[serde(default)]
61 pub max_lifetime_secs: Option<u64>,
62 #[serde(default)]
64 pub query_timeout_secs: Option<u64>,
65 #[serde(default)]
69 pub slow_query_ms: Option<u64>,
70 #[serde(default)]
74 pub test_before_acquire: Option<bool>,
75}
76
77#[derive(Debug, Clone)]
79pub struct PoolParams {
80 pub max_connections: u32,
81 pub min_connections: u32,
82 pub acquire_timeout: Duration,
83 pub idle_timeout: Duration,
84 pub max_lifetime: Duration,
85 pub query_timeout: Option<Duration>,
86 pub test_before_acquire: bool,
87 pub session_init: Vec<String>,
89 pub slow_query: Option<Duration>,
91}
92
93impl Default for PoolParams {
94 fn default() -> Self {
97 Self {
98 max_connections: 10,
99 min_connections: 0,
100 acquire_timeout: Duration::from_secs(30),
101 idle_timeout: Duration::from_secs(600),
102 max_lifetime: Duration::from_secs(1800),
103 query_timeout: Some(Duration::from_secs(30)),
104 test_before_acquire: false,
105 session_init: Vec::new(),
106 slow_query: Some(Duration::from_secs(1)),
107 }
108 }
109}
110
111impl PoolParams {
112 pub fn for_url(url: &str) -> Self {
118 Self {
119 session_init: dialect_session_init(url),
120 ..Self::default()
121 }
122 }
123}
124
125pub fn dialect_session_init(url: &str) -> Vec<String> {
128 match Dialect::from_url(url) {
129 Dialect::Postgres => vec![
130 "SET TIME ZONE 'UTC'".to_string(),
131 "SET application_name = 'ecat'".to_string(),
132 ],
133 Dialect::MySql => vec!["SET time_zone = '+00:00'".to_string()],
134 _ => Vec::new(),
135 }
136}
137
138impl SqlxConfig {
139 pub fn pool(&self) -> PoolParams {
142 let d = PoolParams::default();
143 let max_connections = self.max_connections.unwrap_or(d.max_connections);
144 PoolParams {
145 max_connections,
146 min_connections: self
151 .min_connections
152 .unwrap_or(d.min_connections)
153 .min(max_connections),
154 acquire_timeout: self
155 .acquire_timeout_secs
156 .map_or(d.acquire_timeout, Duration::from_secs),
157 idle_timeout: self
158 .idle_timeout_secs
159 .map_or(d.idle_timeout, Duration::from_secs),
160 max_lifetime: self
161 .max_lifetime_secs
162 .map_or(d.max_lifetime, Duration::from_secs),
163 query_timeout: self.query_timeout(),
164 test_before_acquire: self.test_before_acquire.unwrap_or(d.test_before_acquire),
165 session_init: self.effective_session_init(),
166 slow_query: self.slow_query(),
167 }
168 }
169
170 pub fn query_timeout(&self) -> Option<Duration> {
172 match self.query_timeout_secs {
173 None => Some(Duration::from_secs(30)),
174 Some(0) => None,
175 Some(s) => Some(Duration::from_secs(s)),
176 }
177 }
178
179 pub fn slow_query(&self) -> Option<Duration> {
182 match self.slow_query_ms {
183 None => Some(Duration::from_secs(1)),
184 Some(0) => None,
185 Some(ms) => Some(Duration::from_millis(ms)),
186 }
187 }
188
189 pub fn effective_session_init(&self) -> Vec<String> {
195 self.session_init
196 .clone()
197 .unwrap_or_else(|| dialect_session_init(&self.url))
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn minimal_config_uses_documented_defaults() {
207 let cfg: SqlxConfig =
208 serde_json::from_str(r#"{"url": "postgres://localhost/db"}"#).unwrap();
209 let p = cfg.pool();
210 assert_eq!(p.max_connections, 10);
211 assert_eq!(p.min_connections, 0);
212 assert_eq!(p.acquire_timeout, Duration::from_secs(30));
213 assert_eq!(p.idle_timeout, Duration::from_secs(600));
214 assert_eq!(p.max_lifetime, Duration::from_secs(1800));
215 assert_eq!(p.query_timeout, Some(Duration::from_secs(30)));
216 assert!(!p.test_before_acquire);
217 assert!(cfg.session_init.is_none());
218 assert_eq!(p.slow_query, Some(Duration::from_secs(1)));
219 }
220
221 #[test]
222 fn mysql_compat_is_parsed() {
223 let cfg: SqlxConfig =
224 serde_json::from_str(r#"{"url": "mysql://h:9030/db", "mysql_compat": "starrocks"}"#)
225 .unwrap();
226 assert_eq!(cfg.mysql_compat, Some(MysqlCompat::StarRocks));
227
228 let cfg: SqlxConfig =
229 serde_json::from_str(r#"{"url": "mysql://h:9030/db", "mysql_compat": "doris"}"#)
230 .unwrap();
231 assert_eq!(cfg.mysql_compat, Some(MysqlCompat::Doris));
232 }
233
234 #[test]
235 fn pool_params_can_be_overridden() {
236 let cfg: SqlxConfig = serde_json::from_str(
237 r#"{"url": "mysql://localhost/db",
238 "max_connections": 32,
239 "query_timeout_secs": 0,
240 "test_before_acquire": true,
241 "session_init": ["SET time_zone = '+00:00'"]}"#,
242 )
243 .unwrap();
244 assert_eq!(cfg.pool().max_connections, 32);
245 assert_eq!(cfg.pool().query_timeout, None);
246 assert!(cfg.pool().test_before_acquire);
247 assert_eq!(
248 cfg.effective_session_init(),
249 vec!["SET time_zone = '+00:00'".to_string()]
250 );
251 }
252
253 #[test]
256 fn session_init_defaults_per_dialect() {
257 let pg: SqlxConfig = serde_json::from_str(r#"{"url": "postgres://h/db"}"#).unwrap();
258 assert!(
259 pg.effective_session_init()
260 .iter()
261 .any(|s| s.contains("TIME ZONE"))
262 );
263
264 let sqlite: SqlxConfig = serde_json::from_str(r#"{"url": "sqlite::memory:"}"#).unwrap();
265 assert!(sqlite.effective_session_init().is_empty());
266 }
267
268 #[test]
271 fn for_url_matches_pool_defaults_field_by_field() {
272 for url in ["postgres://h/db", "mysql://h/db", "sqlite::memory:"] {
273 let cfg: SqlxConfig = serde_json::from_str(&format!(r#"{{"url": "{url}"}}"#)).unwrap();
274 let a = PoolParams::for_url(url);
275 let b = cfg.pool();
276 assert_eq!(a.max_connections, b.max_connections, "{url}");
277 assert_eq!(a.min_connections, b.min_connections, "{url}");
278 assert_eq!(a.acquire_timeout, b.acquire_timeout, "{url}");
279 assert_eq!(a.idle_timeout, b.idle_timeout, "{url}");
280 assert_eq!(a.max_lifetime, b.max_lifetime, "{url}");
281 assert_eq!(a.query_timeout, b.query_timeout, "{url}");
282 assert_eq!(a.test_before_acquire, b.test_before_acquire, "{url}");
283 assert_eq!(a.session_init, b.session_init, "{url}");
284 assert_eq!(a.slow_query, b.slow_query, "{url}");
285 }
286 }
287
288 #[test]
290 fn min_connections_is_clamped_to_max() {
291 let cfg: SqlxConfig = serde_json::from_str(
292 r#"{"url": "sqlite::memory:", "max_connections": 2, "min_connections": 5}"#,
293 )
294 .unwrap();
295 let p = cfg.pool();
296 assert_eq!(p.max_connections, 2);
297 assert_eq!(p.min_connections, 2);
298 }
299
300 #[test]
303 fn slow_query_ms_is_threshold_with_zero_meaning_off() {
304 let off: SqlxConfig =
305 serde_json::from_str(r#"{"url": "sqlite::memory:", "slow_query_ms": 0}"#).unwrap();
306 assert_eq!(off.slow_query(), None);
307 assert_eq!(off.pool().slow_query, None);
308
309 let on: SqlxConfig =
310 serde_json::from_str(r#"{"url": "sqlite::memory:", "slow_query_ms": 250}"#).unwrap();
311 assert_eq!(on.slow_query(), Some(Duration::from_millis(250)));
312 assert_eq!(on.pool().slow_query, Some(Duration::from_millis(250)));
313 }
314
315 #[test]
317 fn zero_query_timeout_means_disabled() {
318 let cfg: SqlxConfig =
319 serde_json::from_str(r#"{"url": "sqlite::memory:", "query_timeout_secs": 0}"#).unwrap();
320 assert_eq!(cfg.query_timeout(), None);
321 }
322
323 #[tokio::test]
325 async fn tls_field_is_rejected_not_silently_ignored() {
326 let cfg: SqlxConfig =
327 serde_json::from_str(r#"{"url": "postgres://h/db", "tls": {"skip_verify": true}}"#)
328 .unwrap();
329 assert!(
330 cfg.tls.is_some(),
331 "serde 应当照旧接受该字段(报错发生在 from_config)"
332 );
333
334 let Err(err) = crate::SqlxClient::from_config(cfg).await else {
336 panic!("tls 字段设了必须报错,不能静默忽略");
337 };
338 let msg = err.to_string();
339 assert!(msg.contains("tls"), "got: {msg}");
340 assert!(
341 msg.contains("sslmode"),
342 "错误信息要指向正确做法,got: {msg}"
343 );
344 }
345}