Skip to main content

keelson_sqlx/
mysql.rs

1//! The MySQL driver: [`Pool`] implements
2//! [`Executor`], [`Begin`], [`BeginWith`] and [`StreamExecutor`].
3//!
4//! [`Pool::connect`] pins `time_zone = '+00:00'` on **every** connection it
5//! establishes — the `docs/type-mappings.md` requirement that makes
6//! `TIMESTAMP` an instant rather than a session-relative reading. No
7//! user-visible surface depends on remembering to do this.
8
9use std::sync::Arc;
10
11use keelson_core::Value;
12use keelson_exec::{
13    Begin, BeginWith, Column, ExecError, ExecFuture, ExecResult, Executor, Family, RawConnection,
14    Row, RowStream, Statement, StreamExecutor, Transaction, TxConflict, TxConflictError, TxOptions,
15};
16use sqlx::mysql::{MySqlArguments, MySqlRow};
17use sqlx::{Column as _, MySql, Row as _, TypeInfo as _, ValueRef as _};
18
19use crate::common::{decode_err, unhandled};
20
21/// A MySQL connection pool, with the session zone pinned to UTC on every
22/// connection.
23#[derive(Debug, Clone)]
24pub struct Pool {
25    inner: sqlx::MySqlPool,
26}
27
28impl Pool {
29    /// Connect to `url` (`mysql://user:pass@host:port/db`).
30    pub async fn connect(url: &str) -> Result<Self, ExecError> {
31        let inner = sqlx::mysql::MySqlPoolOptions::new()
32            .after_connect(|conn, _meta| {
33                Box::pin(async move {
34                    // The type-mappings session-zone requirement, applied at
35                    // the only place it cannot be forgotten.
36                    sqlx::Executor::execute(&mut *conn, "SET time_zone = '+00:00'").await?;
37                    Ok(())
38                })
39            })
40            .connect(url)
41            .await
42            .map_err(ExecError::driver)?;
43        Ok(Pool { inner })
44    }
45
46    /// Wrap an existing sqlx pool.
47    ///
48    /// The caller then owns the session-zone pin: connections this pool
49    /// establishes are **not** set to `time_zone = '+00:00'` unless its own
50    /// `after_connect` does so, and `TIMESTAMP` round-trips are wrong without
51    /// it. Prefer [`Pool::connect`].
52    pub fn from_pool(inner: sqlx::MySqlPool) -> Self {
53        Pool { inner }
54    }
55
56    /// The wrapped sqlx pool.
57    pub fn inner(&self) -> &sqlx::MySqlPool {
58        &self.inner
59    }
60}
61
62impl Executor for Pool {
63    fn family(&self) -> Family {
64        Family::MySql
65    }
66
67    fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
68        Box::pin(async move {
69            let Statement { sql, args, .. } = stmt;
70            do_fetch(&self.inner, &sql, args).await
71        })
72    }
73
74    fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
75        Box::pin(async move {
76            let Statement { sql, args, .. } = stmt;
77            do_execute(&self.inner, &sql, args).await
78        })
79    }
80}
81
82impl Begin for Pool {
83    fn begin(&self) -> ExecFuture<'_, Result<Transaction, ExecError>> {
84        Box::pin(async move {
85            let conn = self.inner.acquire().await.map_err(ExecError::driver)?;
86            Transaction::begin_on(Box::new(RawConn { conn })).await
87        })
88    }
89}
90
91impl BeginWith for Pool {
92    fn begin_with(&self, opts: TxOptions) -> ExecFuture<'_, Result<Transaction, ExecError>> {
93        Box::pin(async move {
94            // Refuse before taking a connection out of the pool.
95            opts.check(Family::MySql)?;
96            let conn = self.inner.acquire().await.map_err(ExecError::driver)?;
97            Transaction::begin_on_with(Box::new(RawConn { conn }), opts).await
98        })
99    }
100}
101
102impl StreamExecutor for Pool {
103    fn fetch_stream(&self, stmt: Statement) -> ExecFuture<'_, Result<RowStream, ExecError>> {
104        Box::pin(async move {
105            let pool = self.inner.clone();
106            let (tx, rx) = tokio::sync::mpsc::channel::<Result<Row, ExecError>>(64);
107            tokio::spawn(async move {
108                use futures_util::StreamExt as _;
109                let Statement { sql, args, .. } = stmt;
110                let q = match bind_args(&sql, args) {
111                    Ok(q) => q,
112                    Err(e) => {
113                        let _ = tx.send(Err(e)).await;
114                        return;
115                    }
116                };
117                let mut native = q.fetch(&pool);
118                let mut header: Option<Arc<[Column]>> = None;
119                while let Some(next) = native.next().await {
120                    let msg = match next {
121                        Ok(row) => decode_row(&row, &mut header),
122                        Err(e) => Err(ExecError::driver(e)),
123                    };
124                    let stop = msg.is_err();
125                    if tx.send(msg).await.is_err() || stop {
126                        return;
127                    }
128                }
129            });
130            Ok(RowStream::new(rx))
131        })
132    }
133}
134
135/// One checked-out connection, exclusively held by a [`Transaction`].
136#[derive(Debug)]
137struct RawConn {
138    conn: sqlx::pool::PoolConnection<MySql>,
139}
140
141impl RawConnection for RawConn {
142    fn family(&self) -> Family {
143        Family::MySql
144    }
145
146    fn fetch<'a>(
147        &'a mut self,
148        sql: &'a str,
149        args: Vec<Value>,
150    ) -> ExecFuture<'a, Result<Vec<Row>, ExecError>> {
151        Box::pin(async move { do_fetch(&mut *self.conn, sql, args).await })
152    }
153
154    fn execute<'a>(
155        &'a mut self,
156        sql: &'a str,
157        args: Vec<Value>,
158    ) -> ExecFuture<'a, Result<ExecResult, ExecError>> {
159        Box::pin(async move { do_execute(&mut *self.conn, sql, args).await })
160    }
161
162    fn abandon(self: Box<Self>) {
163        let _ = self.conn.detach();
164    }
165}
166
167/// A driver failure, with concurrency conflicts classified out of it.
168///
169/// MySQL uses SQLSTATE as a coarse category and the error *number* as the
170/// precise one, so the number is what is matched: `1213` `ER_LOCK_DEADLOCK`
171/// (SQLSTATE `40001` — a deadlock is how InnoDB reports a serialization
172/// failure) and `1205` `ER_LOCK_WAIT_TIMEOUT`. Everything else stays an
173/// opaque driver error.
174fn driver_err(e: sqlx::Error) -> ExecError {
175    if let sqlx::Error::Database(db) = &e
176        && let Some(my) = db.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>()
177    {
178        let kind = match my.number() {
179            1213 => Some(TxConflict::Deadlock),
180            1205 => Some(TxConflict::LockTimeout),
181            _ => None,
182        };
183        if let Some(kind) = kind {
184            let code = my.number().to_string();
185            let message = my.message().to_owned();
186            return TxConflictError::new(kind, code, message)
187                .with_source(e)
188                .into_exec_error();
189        }
190    }
191    ExecError::driver(e)
192}
193
194async fn do_fetch<'e, E>(exec: E, sql: &str, args: Vec<Value>) -> Result<Vec<Row>, ExecError>
195where
196    E: sqlx::Executor<'e, Database = MySql>,
197{
198    let rows = bind_args(sql, args)?
199        .fetch_all(exec)
200        .await
201        .map_err(driver_err)?;
202    let mut header: Option<Arc<[Column]>> = None;
203    rows.iter().map(|r| decode_row(r, &mut header)).collect()
204}
205
206async fn do_execute<'e, E>(exec: E, sql: &str, args: Vec<Value>) -> Result<ExecResult, ExecError>
207where
208    E: sqlx::Executor<'e, Database = MySql>,
209{
210    // Zero-argument statements go over the driver's plain (unprepared) path:
211    // MySQL refuses transaction control (`BEGIN`, `SAVEPOINT …`) in the
212    // prepared-statement protocol, and nothing is gained by preparing an
213    // argument-less statement anyway.
214    let done = if args.is_empty() {
215        exec.execute(sql).await.map_err(driver_err)?
216    } else {
217        bind_args(sql, args)?
218            .execute(exec)
219            .await
220            .map_err(driver_err)?
221    };
222    let last = i64::try_from(done.last_insert_id())
223        .ok()
224        .filter(|id| *id != 0);
225    Ok(ExecResult::new(done.rows_affected(), last))
226}
227
228/// The total map `Value` → MySQL parameter, per the "binds as" column of
229/// `docs/type-mappings.md`. Notably: `Uuid` binds as hyphenated lowercase
230/// text (the `CHAR(36)` mapping), and `TimestampTz` relies on the session
231/// zone pinned by [`Pool::connect`].
232fn bind_args<'q>(
233    sql: &'q str,
234    args: Vec<Value>,
235) -> Result<sqlx::query::Query<'q, MySql, MySqlArguments>, ExecError> {
236    let mut q = sqlx::query(sql);
237    for v in args {
238        q = match v {
239            Value::Null => q.bind(Option::<String>::None),
240            Value::Bool(x) => q.bind(x),
241            Value::I8(x) => q.bind(x),
242            Value::I16(x) => q.bind(x),
243            Value::I32(x) => q.bind(x),
244            Value::I64(x) => q.bind(x),
245            Value::U8(x) => q.bind(x),
246            Value::U16(x) => q.bind(x),
247            Value::U32(x) => q.bind(x),
248            Value::U64(x) => q.bind(x),
249            Value::F32(x) => q.bind(x),
250            Value::F64(x) => q.bind(x),
251            Value::Text(x) => q.bind(x),
252            Value::Bytes(x) => q.bind(x),
253            #[cfg(feature = "chrono")]
254            Value::Date(x) => q.bind(x),
255            #[cfg(feature = "chrono")]
256            Value::Time(x) => q.bind(x),
257            #[cfg(feature = "chrono")]
258            Value::DateTime(x) => q.bind(x),
259            #[cfg(feature = "chrono")]
260            Value::TimestampTz(x) => q.bind(x),
261            #[cfg(feature = "uuid")]
262            Value::Uuid(x) => q.bind(x.hyphenated().to_string()),
263            #[cfg(feature = "decimal")]
264            Value::Decimal(x) => q.bind(x),
265            #[cfg(feature = "json")]
266            Value::Json(x) => q.bind(x),
267            other => {
268                return Err(ExecError::UnsupportedValue {
269                    type_name: other.type_name(),
270                    family: Family::MySql,
271                });
272            }
273        };
274    }
275    Ok(q)
276}
277
278fn decode_row(row: &MySqlRow, header: &mut Option<Arc<[Column]>>) -> Result<Row, ExecError> {
279    let columns = header
280        .get_or_insert_with(|| {
281            row.columns()
282                .iter()
283                .map(|c| Column::new(c.name()))
284                .collect::<Vec<_>>()
285                .into()
286        })
287        .clone();
288    let mut values = Vec::with_capacity(row.columns().len());
289    for i in 0..row.columns().len() {
290        values.push(decode_value(row, i)?);
291    }
292    Ok(Row::new(columns, values))
293}
294
295fn decode_value(row: &MySqlRow, i: usize) -> Result<Value, ExecError> {
296    let col = &row.columns()[i];
297    let name = col.name();
298    let raw = row.try_get_raw(i).map_err(|e| decode_err(name, e))?;
299    if raw.is_null() {
300        return Ok(Value::Null);
301    }
302    let ty = raw.type_info();
303    let ty = ty.name();
304
305    macro_rules! take {
306        ($t:ty) => {
307            row.try_get::<$t, _>(i).map_err(|e| decode_err(name, e))?
308        };
309    }
310
311    Ok(match ty {
312        // TINYINT(1); what the mappings table means by a boolean column.
313        "BOOLEAN" => Value::Bool(take!(bool)),
314        "TINYINT" => Value::I8(take!(i8)),
315        "SMALLINT" => Value::I16(take!(i16)),
316        "MEDIUMINT" | "INT" => Value::I32(take!(i32)),
317        "BIGINT" => Value::I64(take!(i64)),
318        "TINYINT UNSIGNED" => Value::U8(take!(u8)),
319        "SMALLINT UNSIGNED" => Value::U16(take!(u16)),
320        "MEDIUMINT UNSIGNED" | "INT UNSIGNED" => Value::U32(take!(u32)),
321        "BIGINT UNSIGNED" => Value::U64(take!(u64)),
322        "FLOAT" => Value::F32(take!(f32)),
323        "DOUBLE" => Value::F64(take!(f64)),
324        "CHAR" | "VARCHAR" | "TEXT" | "TINYTEXT" | "MEDIUMTEXT" | "LONGTEXT" | "ENUM" => {
325            Value::Text(take!(String))
326        }
327        "BINARY" | "VARBINARY" | "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" => {
328            Value::Bytes(take!(Vec<u8>))
329        }
330        #[cfg(feature = "chrono")]
331        "DATE" => Value::Date(take!(chrono::NaiveDate)),
332        #[cfg(feature = "chrono")]
333        "TIME" => Value::Time(take!(chrono::NaiveTime)),
334        #[cfg(feature = "chrono")]
335        "DATETIME" => Value::DateTime(take!(chrono::NaiveDateTime)),
336        // With the session zone pinned to +00:00, a TIMESTAMP comes back as
337        // the instant it stores.
338        #[cfg(feature = "chrono")]
339        "TIMESTAMP" => Value::TimestampTz(take!(chrono::DateTime<chrono::Utc>)),
340        #[cfg(not(feature = "chrono"))]
341        "DATE" | "TIME" | "DATETIME" | "TIMESTAMP" => {
342            return Err(crate::common::need_feature(name, ty, "chrono"));
343        }
344        #[cfg(feature = "decimal")]
345        "DECIMAL" => Value::Decimal(take!(rust_decimal::Decimal)),
346        #[cfg(not(feature = "decimal"))]
347        "DECIMAL" => return Err(crate::common::need_feature(name, ty, "decimal")),
348        #[cfg(feature = "json")]
349        "JSON" => Value::Json(take!(serde_json::Value)),
350        #[cfg(not(feature = "json"))]
351        "JSON" => return Err(crate::common::need_feature(name, ty, "json")),
352        other => return Err(unhandled(name, other)),
353    })
354}