Skip to main content

runlight_sqlx/
mysql.rs

1//! MySQL 8.4 and MariaDB 11.4, on the app's pool. Each statement runs on the pool on its own; a
2//! transaction or a lock holds one connection until it ends.
3
4use std::sync::Arc;
5
6use runlight::BoxFuture;
7use runlight::store::{Db, DbError, Dialect, Hold, Param, Row, fill_placeholders, mysql_literal};
8use sqlx::pool::PoolConnection;
9use sqlx::{AssertSqlSafe, MySql, MySqlPool};
10use tokio::sync::Mutex;
11
12use crate::rows::{db_error, mysql_row};
13
14/// One lock per database, so installs sharing a server do not wait on each other. Lock names are 64
15/// characters at most.
16const LOCK: &str = "CONCAT('runlight:', LEFT(SHA2(COALESCE(DATABASE(), ''), 256), 48))";
17
18/// The store's driver over a MySQL pool. SQL written for SQLite and Postgres is sent as MySQL reads it:
19/// a "quoted" name in backticks, a backslash inside 'text' doubled, and each value written into the
20/// statement, escaped, as mysql2 fills them in.
21#[derive(Clone)]
22pub struct MysqlDb {
23    pool: MySqlPool,
24}
25
26impl MysqlDb {
27    /// The store's driver over a pool.
28    pub fn new(pool: MySqlPool) -> MysqlDb {
29        MysqlDb { pool }
30    }
31}
32
33/// The session's sql_mode as it was, less NO_BACKSLASH_ESCAPES, wherever it sits in the list.
34const NO_BACKSLASH_ESCAPES_OFF: &str = "SET SESSION sql_mode = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', @@SESSION.sql_mode, ','), ',NO_BACKSLASH_ESCAPES,', ','))";
35
36/// A statement with its values written in. Values are escaped with backslashes, which a server set to
37/// NO_BACKSLASH_ESCAPES reads as text, so a quote could end the value early: the statement goes after
38/// one that drops that mode from the connection, an app's own pool's too, in the same round trip.
39fn text(sql: &str, params: &[Param]) -> Result<AssertSqlSafe<String>, DbError> {
40    Ok(AssertSqlSafe(format!("{NO_BACKSLASH_ESCAPES_OFF}; {}", fill_placeholders(sql, params, true, mysql_literal)?)))
41}
42
43async fn exec(conn: &mut PoolConnection<MySql>, sql: &str) -> Result<Vec<Row>, DbError> {
44    let rows = sqlx::raw_sql(AssertSqlSafe(sql.to_string())).fetch_all(&mut **conn).await.map_err(db_error)?;
45    rows.iter().map(mysql_row).collect()
46}
47
48impl Db for MysqlDb {
49    fn dialect(&self) -> Dialect {
50        Dialect::Mysql
51    }
52
53    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
54        Box::pin(async move {
55            let rows = sqlx::raw_sql(text(sql, &params)?).fetch_all(&self.pool).await.map_err(db_error)?;
56            rows.iter().map(mysql_row).collect()
57        })
58    }
59
60    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
61        Box::pin(async move {
62            Ok(sqlx::raw_sql(text(sql, &params)?).execute(&self.pool).await.map_err(db_error)?.rows_affected())
63        })
64    }
65
66    fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
67        Box::pin(async move {
68            let mut conn = self.pool.acquire().await.map_err(db_error)?;
69            match kind {
70                Hold::Transaction => begin(&mut conn).await?,
71                Hold::Exclusive => loop {
72                    let rows = exec(&mut conn, &format!("SELECT GET_LOCK({LOCK}, 5) AS ok")).await?;
73                    let row = rows.into_iter().next().unwrap_or_default();
74                    if row.is_null("ok") {
75                        return Err(DbError("Runlight: MySQL refused the lock for creating tables".into()));
76                    }
77                    if row.num("ok") == 1.0 {
78                        break;
79                    }
80                    // Not got within 5 seconds: another process is creating the tables. Ask again.
81                },
82            }
83            Ok(Arc::new(Held { conn: Arc::new(Mutex::new(Some(conn))), kind, role: Role::Owner }) as Arc<dyn Db>)
84        })
85    }
86}
87
88/// As Postgres does by default: each statement sees what was committed before it began, and InnoDB
89/// takes no gap locks, so two writers to neighbouring rows do not deadlock.
90async fn begin(conn: &mut PoolConnection<MySql>) -> Result<(), DbError> {
91    exec(conn, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED").await?;
92    exec(conn, "START TRANSACTION").await?;
93    Ok(())
94}
95
96/// One connection held for a transaction or a lock.
97struct Held {
98    conn: Arc<Mutex<Option<PoolConnection<MySql>>>>,
99    kind: Hold,
100    role: Role,
101}
102
103/// What finishing a held connection does.
104#[derive(Clone, Copy, PartialEq, Eq)]
105enum Role {
106    /// Ends the transaction or lets the lock go, and gives the connection back.
107    Owner,
108    /// A transaction begun inside a lock: ends the transaction, and keeps the connection.
109    Nested,
110    /// Joined what was already held: ends nothing.
111    Joined,
112}
113
114fn ended() -> DbError {
115    DbError("Runlight: the held connection has been let go".into())
116}
117
118impl Db for Held {
119    fn dialect(&self) -> Dialect {
120        Dialect::Mysql
121    }
122
123    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
124        Box::pin(async move {
125            let mut guard = self.conn.lock().await;
126            let conn = guard.as_mut().ok_or_else(ended)?;
127            let rows = sqlx::raw_sql(text(sql, &params)?).fetch_all(&mut **conn).await.map_err(db_error)?;
128            rows.iter().map(mysql_row).collect()
129        })
130    }
131
132    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
133        Box::pin(async move {
134            let mut guard = self.conn.lock().await;
135            let conn = guard.as_mut().ok_or_else(ended)?;
136            Ok(sqlx::raw_sql(text(sql, &params)?).execute(&mut **conn).await.map_err(db_error)?.rows_affected())
137        })
138    }
139
140    fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
141        Box::pin(async move {
142            if kind == Hold::Transaction && self.kind == Hold::Exclusive {
143                let mut guard = self.conn.lock().await;
144                begin(guard.as_mut().ok_or_else(ended)?).await?;
145                return Ok(Arc::new(Held { conn: self.conn.clone(), kind, role: Role::Nested }) as Arc<dyn Db>);
146            }
147            Ok(Arc::new(Held { conn: self.conn.clone(), kind: self.kind, role: Role::Joined }) as Arc<dyn Db>)
148        })
149    }
150
151    fn finish(&self, ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
152        Box::pin(async move {
153            let mut guard = self.conn.lock().await;
154            match self.role {
155                Role::Joined => Ok(()),
156                Role::Nested => {
157                    let Some(conn) = guard.as_mut() else { return Ok(()) };
158                    exec(conn, if ok { "COMMIT" } else { "ROLLBACK" }).await.map(|_| ())
159                }
160                Role::Owner => {
161                    let Some(mut conn) = guard.take() else { return Ok(()) };
162                    let result = match self.kind {
163                        Hold::Transaction => exec(&mut conn, if ok { "COMMIT" } else { "ROLLBACK" }).await.map(|_| ()),
164                        // A lost connection ends its session, and the lock with it.
165                        Hold::Exclusive => {
166                            let _ = exec(&mut conn, &format!("DO RELEASE_LOCK({LOCK})")).await;
167                            Ok(())
168                        }
169                    };
170                    if result.is_err() {
171                        // A connection still in its transaction must never go back to the pool.
172                        conn.detach();
173                    }
174                    result
175                }
176            }
177        })
178    }
179}