use super::queries::AllowedMySqlQuery;
use anyhow::{anyhow, Context, Result};
use mysql_async::prelude::*;
use mysql_async::{Conn, OptsBuilder, Row};
pub struct ReadOnlyMySqlConn {
conn: Conn,
}
impl ReadOnlyMySqlConn {
pub async fn connect(url: &str) -> Result<Self> {
let opts = OptsBuilder::from_opts(
mysql_async::Opts::from_url(url)
.with_context(|| "mysql_async::Opts::from_url failed")?,
);
let mut conn = Conn::new(opts)
.await
.with_context(|| "mysql_async::Conn::new failed")?;
for stmt in [
"SET SESSION TRANSACTION READ ONLY",
"SET SESSION MAX_EXECUTION_TIME = 500",
"SET SESSION innodb_lock_wait_timeout = 1",
] {
conn.query_drop(stmt)
.await
.with_context(|| format!("failed to issue {stmt}"))?;
}
let tx_read_only: Option<String> = conn
.query_first("SELECT @@SESSION.transaction_read_only")
.await
.with_context(|| "failed to read @@SESSION.transaction_read_only")?;
match tx_read_only.as_deref() {
Some("1") | Some("ON") => {}
other => {
return Err(anyhow!(
"refusing to proceed: @@SESSION.transaction_read_only = {:?}, expected 1/ON",
other
))
}
}
let max_exec: Option<u64> = conn
.query_first("SELECT @@SESSION.MAX_EXECUTION_TIME")
.await
.with_context(|| "failed to read @@SESSION.MAX_EXECUTION_TIME")?;
if max_exec != Some(500) {
return Err(anyhow!(
"refusing to proceed: @@SESSION.MAX_EXECUTION_TIME = {:?}, expected 500",
max_exec
));
}
let lock_wait: Option<u64> = conn
.query_first("SELECT @@SESSION.innodb_lock_wait_timeout")
.await
.with_context(|| "failed to read @@SESSION.innodb_lock_wait_timeout")?;
if lock_wait != Some(1) {
return Err(anyhow!(
"refusing to proceed: @@SESSION.innodb_lock_wait_timeout = {:?}, expected 1",
lock_wait
));
}
Ok(Self { conn })
}
pub async fn query_allowed(&mut self, q: AllowedMySqlQuery) -> Result<Vec<Row>> {
let sql = q.sql();
self.conn
.query(sql)
.await
.with_context(|| format!("query_allowed failed for {:?}", q))
}
pub async fn disconnect(self) -> Result<()> {
self.conn
.disconnect()
.await
.with_context(|| "mysql_async Conn::disconnect failed")
}
}