1mod codec;
14#[cfg(test)]
15mod contract;
16mod lease;
17#[cfg(test)]
18mod options_tests;
19
20use super::io::MySqlIo;
21use super::metadata::TableCache;
22use super::returning::{Prepared, prepare, synthesize, synthesized_result};
23use super::returning::{table_of, write_returning};
24use crate::shared::affected_result;
25use crate::sql::{CompiledQuery, MySql, compile, compile_write};
26use async_trait::async_trait;
27use lease::Lease;
28use mysql_async::{Opts, OptsBuilder, Pool, PoolConstraints, PoolOpts};
29use siderite_orm::{Backend, BackendCapabilities, BackendError, ExecResult};
30use siderite_orm::{Executor, IsolationLevel, OrmError, QueryError, QueryPlan};
31use siderite_orm::{QueryResult, Transaction, Value, WritePlan};
32use std::fmt::{self, Debug, Formatter};
33use std::sync::Arc;
34use std::time::Duration;
35use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard, Semaphore};
36
37type Reply<T> = Result<T, OrmError>;
38type Args = Vec<Value>;
39type Scope = Box<dyn Transaction>;
40type ReadResult = Reply<QueryResult>;
41
42#[derive(Debug, Clone)]
44pub struct NativeMySqlOptions {
45 pub max_connections: usize,
47 pub max_waiters: usize,
49 pub acquire_timeout: Duration,
51}
52
53impl Default for NativeMySqlOptions {
54 fn default() -> Self {
55 Self {
56 max_connections: 10,
57 max_waiters: 100,
58 acquire_timeout: Duration::from_secs(10),
59 }
60 }
61}
62
63#[derive(Clone)]
65pub struct NativeMySqlBackend {
66 pool: Pool,
67 admission: Arc<Semaphore>,
68 acquire_timeout: Duration,
69 max_connections: usize,
70 tables: Arc<TableCache>,
71}
72
73impl NativeMySqlBackend {
74 pub async fn connect(url: &str) -> Result<Self, BackendError> {
82 Self::connect_with(url, NativeMySqlOptions::default()).await
83 }
84
85 pub async fn connect_with(
94 url: &str,
95 options: NativeMySqlOptions,
96 ) -> Result<Self, BackendError> {
97 let parsed = Opts::from_url(url).map_err(|_| {
98 let message = "invalid native MySQL URL";
99 connection_error(message)
100 })?;
101 Self::connect_options(parsed, options).await
102 }
103
104 pub async fn connect_options(
117 connection: Opts,
118 options: NativeMySqlOptions,
119 ) -> Result<Self, BackendError> {
120 let capacity = options
121 .max_connections
122 .checked_add(options.max_waiters)
123 .filter(|capacity| *capacity <= Semaphore::MAX_PERMITS)
124 .ok_or_else(|| connection_error("invalid admission capacity"))?;
125 let count = options.max_connections;
128 let bounds = PoolConstraints::new(count, count)
129 .filter(|_| options.max_connections > 0)
130 .ok_or_else(|| connection_error("invalid connection count"))?;
131 if options.acquire_timeout.is_zero() {
132 return Err(connection_error("checkout deadline must be positive"));
133 }
134 let mut setup = super::SESSION_SETUP.to_vec();
135 setup.push("SET autocommit = 1");
136 let acquire_timeout = options.acquire_timeout;
137 let connect_options = OptsBuilder::from_opts(connection)
138 .client_found_rows(true)
139 .stmt_cache_size(128)
140 .setup(setup)
141 .pool_opts(
142 PoolOpts::default()
143 .with_constraints(bounds)
144 .with_reset_connection(false),
145 );
146 let backend = Self {
147 pool: Pool::new(connect_options),
148 admission: Arc::new(Semaphore::new(capacity)),
149 acquire_timeout,
150 max_connections: count,
151 tables: Arc::default(),
152 };
153 backend.warm().await?;
154 Ok(backend)
155 }
156
157 pub async fn close(&self) -> Result<(), BackendError> {
162 self.pool
163 .clone()
164 .disconnect()
165 .await
166 .map_err(|error| connection_error(error.to_string()))
167 }
168
169 async fn checkout(&self) -> Result<Lease, OrmError> {
170 let permit = Arc::clone(&self.admission)
171 .try_acquire_owned()
172 .map_err(|_| connection_error("native MySQL admission is full"))?;
173 let wait = self.pool.get_conn();
174 let conn = tokio::time::timeout(self.acquire_timeout, wait)
175 .await
176 .map_err(|_| connection_error("native MySQL checkout timed out"))?
177 .map_err(lease::error)?;
178 Ok(Lease::new(conn, permit))
179 }
180
181 pub async fn warm(&self) -> Result<(), BackendError> {
186 let work = async {
187 let mut leases = Vec::with_capacity(self.max_connections);
188 for _ in 0..self.max_connections {
189 leases.push(self.checkout().await?);
190 }
191 Ok::<_, OrmError>(leases)
192 };
193 let leases = tokio::time::timeout(self.acquire_timeout, work)
194 .await
195 .map_err(|_| connection_error("native pool warming timed out"))?
196 .map_err(|error| connection_error(error.to_string()))?;
197 drop(leases);
198 Ok(())
199 }
200
201 fn autocommit(&self) -> bool {
202 true
203 }
204}
205
206trait LeaseAccess {
207 fn lease(&mut self) -> &mut Lease;
208}
209
210impl Debug for NativeMySqlBackend {
211 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
212 formatter
214 .debug_struct("NativeMySqlBackend")
215 .field("max_connections", &self.max_connections)
216 .field("acquire_timeout", &self.acquire_timeout)
217 .finish_non_exhaustive()
218 }
219}
220
221impl LeaseAccess for Lease {
222 fn lease(&mut self) -> &mut Lease {
223 self
224 }
225}
226
227impl LeaseAccess for MappedMutexGuard<'_, Lease> {
228 fn lease(&mut self) -> &mut Lease {
229 self
230 }
231}
232
233macro_rules! executor {
235 ($target:ty) => {
236 #[async_trait]
237 impl Executor for $target {
238 fn capabilities(&self) -> BackendCapabilities {
239 BackendCapabilities::mysql()
240 }
241
242 async fn fetch(&self, plan: &QueryPlan) -> Reply<QueryResult> {
243 let query = compile(plan, &MySql)?;
244 let mut access = self.checkout().await?;
245 access.lease().fetch(&query.sql, query.params).await
246 }
247
248 async fn execute(&self, plan: &WritePlan) -> Reply<ExecResult> {
249 let prepared = Write::prepare(plan)?;
250 let mut access = self.checkout().await?;
251 write(
252 access.lease(),
253 &self.tables,
254 plan,
255 prepared,
256 self.autocommit(),
257 )
258 .await
259 }
260
261 async fn fetch_raw(&self, sql: &str, params: Args) -> ReadResult {
262 self.tables.clear();
263 let mut access = self.checkout().await?;
264 let conn = access.lease();
265 conn.retire = true;
266 conn.fetch(sql, params).await
267 }
268
269 async fn execute_raw(&self, sql: &str, args: Args) -> Reply<u64> {
270 self.tables.clear();
271 let mut access = self.checkout().await?;
272 let conn = access.lease();
273 conn.retire = true;
274 conn.run(sql, args).await.map(|done| done.rows_affected())
275 }
276
277 async fn execute_script(&self, sql: &str) -> Reply<()> {
278 self.tables.clear();
279 let mut access = self.checkout().await?;
280 let conn = access.lease();
281 conn.retire = true;
282 conn.control(sql).await
283 }
284 }
285 };
286}
287
288executor!(NativeMySqlBackend);
289executor!(NativeTransaction);
290
291enum Write {
292 Plain(CompiledQuery),
293 Returning(Prepared),
294}
295
296impl Write {
297 fn prepare(plan: &WritePlan) -> Result<Self, OrmError> {
298 if plan.returning().is_empty() {
299 compile_write(plan, &MySql).map(Self::Plain)
300 } else {
301 prepare(plan).map(Self::Returning)
302 }
303 }
304}
305
306async fn write(
307 conn: &mut Lease,
308 tables: &TableCache,
309 plan: &WritePlan,
310 prepared: Write,
311 autocommit: bool,
312) -> Reply<ExecResult> {
313 let prepared = match prepared {
314 Write::Returning(prepared) => prepared,
315 Write::Plain(query) => {
316 let done = conn.run(&query.sql, query.params).await?;
317 return Ok(affected_result(done.rows_affected()));
318 }
319 };
320 let info = tables.table(conn, table_of(plan)).await?;
321 if autocommit {
322 if let WritePlan::Insert(insert) = plan
323 && let Some(row) = synthesize(&info, insert)
324 {
325 let write = prepared.write;
326 let done = conn.run(&write.sql, write.params).await?;
327 return synthesized_result(&info, row, &done);
328 }
329 begin(conn, None).await?;
330 }
331 let result = write_returning(conn, &info, plan, prepared).await?;
332 if autocommit {
333 conn.control("COMMIT").await?;
334 conn.in_transaction = false;
335 }
336 Ok(result)
337}
338
339#[async_trait]
340impl Backend for NativeMySqlBackend {
341 fn read_parameter_count(&self, plan: &QueryPlan) -> Result<Option<usize>, OrmError> {
342 Ok(Some(compile(plan, &MySql)?.params.len()))
343 }
344
345 async fn begin(&self, isolation: Option<IsolationLevel>) -> Reply<Scope> {
346 if let Some(level) = isolation {
347 self.capabilities()
348 .require(siderite_orm::Feature::Isolation(level))?;
349 }
350 let mut conn = self.checkout().await?;
351 begin(&mut conn, isolation).await?;
352 Ok(Box::new(NativeTransaction {
353 slot: Mutex::new(Some(conn)),
354 tables: Arc::clone(&self.tables),
355 transactional: true,
356 }))
357 }
358
359 async fn begin_schema(&self, transactional: bool) -> Reply<Scope> {
360 if transactional {
361 return self.begin(None).await;
362 }
363 let mut conn = self.checkout().await?;
364 conn.retire = true;
365 Ok(Box::new(NativeTransaction {
366 slot: Mutex::new(Some(conn)),
367 tables: Arc::clone(&self.tables),
368 transactional: false,
369 }))
370 }
371}
372
373async fn begin(conn: &mut Lease, level: Option<IsolationLevel>) -> Reply<()> {
374 conn.in_transaction = true;
375 if let Some(level) = level {
376 let sql = format!(
377 "SET TRANSACTION ISOLATION LEVEL {}",
378 super::isolation_sql(level)
379 );
380 conn.control(&sql).await?;
381 }
382 conn.control("START TRANSACTION").await
383}
384
385struct NativeTransaction {
386 slot: Mutex<Option<Lease>>,
387 tables: Arc<TableCache>,
388 transactional: bool,
389}
390
391impl NativeTransaction {
392 async fn checkout(&self) -> Result<MappedMutexGuard<'_, Lease>, OrmError> {
393 MutexGuard::try_map(self.slot.lock().await, Option::as_mut)
394 .map_err(|_| QueryError::TransactionClosed.into())
395 }
396
397 fn autocommit(&self) -> bool {
398 false
399 }
400
401 async fn finish(&self, commit: bool) -> Reply<()> {
402 let mut conn = self
403 .slot
404 .lock()
405 .await
406 .take()
407 .ok_or(QueryError::TransactionClosed)?;
408 if self.transactional {
409 let sql = if commit { "COMMIT" } else { "ROLLBACK" };
410 conn.control(sql).await?;
411 conn.in_transaction = false;
412 }
413 Ok(())
414 }
415}
416
417#[async_trait]
418impl Transaction for NativeTransaction {
419 async fn commit(&self) -> Reply<()> {
420 self.finish(true).await
421 }
422
423 async fn rollback(&self) -> Reply<()> {
424 self.finish(false).await
425 }
426}
427
428fn connection_error(message: impl Into<String>) -> BackendError {
429 BackendError::Connection(message.into())
430}