1use crate::config::PoolParams;
4use ecat_data::Dialect;
5use sqlx::mysql::{MySqlPool, MySqlPoolOptions};
6use sqlx::postgres::{PgPool, PgPoolOptions};
7use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
8
9#[derive(Debug)]
11pub enum Pool {
12 Pg(PgPool),
13 My(MySqlPool),
14 Sq(SqlitePool),
15}
16
17pub enum PoolGuard {
20 Pg(sqlx::pool::PoolConnection<sqlx::Postgres>),
21 My(sqlx::pool::PoolConnection<sqlx::MySql>),
22 Sq(sqlx::pool::PoolConnection<sqlx::Sqlite>),
23}
24
25impl Pool {
26 pub async fn connect(url: &str, params: &PoolParams) -> Result<Self, sqlx::Error> {
28 match Dialect::from_url(url) {
29 Dialect::Postgres => Ok(Self::Pg(
30 common_pg(PgPoolOptions::new(), params).connect(url).await?,
31 )),
32 Dialect::MySql => Ok(Self::My(
33 common_mysql(MySqlPoolOptions::new(), params)
34 .connect(url)
35 .await?,
36 )),
37 Dialect::Sqlite => Ok(Self::Sq(
38 common_sqlite(SqlitePoolOptions::new(), params)
39 .connect(url)
40 .await?,
41 )),
42 Dialect::Standard => Err(unsupported(url)),
45 Dialect::Mssql => Err(sqlx::Error::Configuration(Box::new(std::io::Error::new(
47 std::io::ErrorKind::InvalidInput,
48 "mssql:// is served by ecat-data-mssql, not the sqlx backend",
49 )))),
50 }
51 }
52
53 pub fn dialect(&self) -> Dialect {
54 match self {
55 Self::Pg(_) => Dialect::Postgres,
56 Self::My(_) => Dialect::MySql,
57 Self::Sq(_) => Dialect::Sqlite,
58 }
59 }
60
61 pub fn size(&self) -> u32 {
63 match self {
64 Self::Pg(p) => p.size(),
65 Self::My(p) => p.size(),
66 Self::Sq(p) => p.size(),
67 }
68 }
69
70 pub fn idle(&self) -> u32 {
72 match self {
73 Self::Pg(p) => p.num_idle() as u32,
74 Self::My(p) => p.num_idle() as u32,
75 Self::Sq(p) => p.num_idle() as u32,
76 }
77 }
78
79 pub async fn acquire(&self) -> Result<PoolGuard, sqlx::Error> {
81 Ok(match self {
82 Self::Pg(p) => PoolGuard::Pg(p.acquire().await?),
83 Self::My(p) => PoolGuard::My(p.acquire().await?),
84 Self::Sq(p) => PoolGuard::Sq(p.acquire().await?),
85 })
86 }
87}
88
89fn unsupported(url: &str) -> sqlx::Error {
91 let scheme = url
92 .split("://")
93 .next()
94 .unwrap_or(url)
95 .split(':')
96 .next()
97 .unwrap_or(url);
98 sqlx::Error::Configuration(Box::new(std::io::Error::new(
99 std::io::ErrorKind::InvalidInput,
100 format!("unsupported database scheme for sqlx backend: {scheme}"),
101 )))
102}
103
104fn common_pg(opts: PgPoolOptions, p: &PoolParams) -> PgPoolOptions {
108 let opts = opts
109 .max_connections(p.max_connections)
110 .min_connections(p.min_connections)
111 .acquire_timeout(p.acquire_timeout)
112 .idle_timeout(Some(p.idle_timeout))
113 .max_lifetime(Some(p.max_lifetime))
114 .test_before_acquire(p.test_before_acquire);
115 if p.session_init.is_empty() {
116 return opts;
117 }
118 let stmts = p.session_init.clone();
119 opts.after_connect(move |conn, _meta| {
120 let stmts = stmts.clone();
121 Box::pin(async move {
122 for s in &stmts {
123 sqlx::query(s).execute(&mut *conn).await?;
124 }
125 Ok(())
126 })
127 })
128}
129
130fn common_mysql(opts: MySqlPoolOptions, p: &PoolParams) -> MySqlPoolOptions {
131 let opts = opts
132 .max_connections(p.max_connections)
133 .min_connections(p.min_connections)
134 .acquire_timeout(p.acquire_timeout)
135 .idle_timeout(Some(p.idle_timeout))
136 .max_lifetime(Some(p.max_lifetime))
137 .test_before_acquire(p.test_before_acquire);
138 if p.session_init.is_empty() {
139 return opts;
140 }
141 let stmts = p.session_init.clone();
142 opts.after_connect(move |conn, _meta| {
143 let stmts = stmts.clone();
144 Box::pin(async move {
145 for s in &stmts {
146 sqlx::query(s).execute(&mut *conn).await?;
147 }
148 Ok(())
149 })
150 })
151}
152
153fn common_sqlite(opts: SqlitePoolOptions, p: &PoolParams) -> SqlitePoolOptions {
154 let opts = opts
155 .max_connections(p.max_connections)
156 .min_connections(p.min_connections)
157 .acquire_timeout(p.acquire_timeout)
158 .idle_timeout(Some(p.idle_timeout))
159 .max_lifetime(Some(p.max_lifetime))
160 .test_before_acquire(p.test_before_acquire);
161 if p.session_init.is_empty() {
162 return opts;
163 }
164 let stmts = p.session_init.clone();
165 opts.after_connect(move |conn, _meta| {
166 let stmts = stmts.clone();
167 Box::pin(async move {
168 for s in &stmts {
169 sqlx::query(s).execute(&mut *conn).await?;
170 }
171 Ok(())
172 })
173 })
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use ecat_data::Dialect;
180
181 #[tokio::test]
182 async fn sqlite_url_builds_sqlite_pool() {
183 let pool = Pool::connect("sqlite::memory:", &PoolParams::for_url("sqlite::memory:"))
184 .await
185 .unwrap();
186 assert_eq!(pool.dialect(), Dialect::Sqlite);
187 assert!(matches!(pool, Pool::Sq(_)));
188 }
189
190 #[test]
192 fn unsupported_scheme_is_rejected_and_names_the_scheme() {
193 let rt = tokio::runtime::Builder::new_current_thread()
194 .enable_all()
195 .build()
196 .unwrap();
197 let err = rt
198 .block_on(Pool::connect("oracle://h/db", &PoolParams::default()))
199 .unwrap_err();
200 let msg = err.to_string();
201 assert!(msg.contains("unsupported"), "got: {msg}");
202 assert!(
203 msg.contains("oracle"),
204 "错误信息必须点名 scheme,got: {msg}"
205 );
206 }
207
208 #[test]
210 fn mssql_scheme_is_rejected_by_sqlx_backend() {
211 let rt = tokio::runtime::Builder::new_current_thread()
212 .enable_all()
213 .build()
214 .unwrap();
215 let err = rt
216 .block_on(Pool::connect("mssql://h:1433/db", &PoolParams::default()))
217 .unwrap_err();
218 assert!(err.to_string().contains("ecat-data-mssql"), "got: {err}");
219 }
220}