1#![warn(missing_docs)]
2#![allow(clippy::needless_range_loop)]
3
4mod value;
16pub(crate) use value::Value;
17
18use async_trait::async_trait;
19use sqlx_core::{
20 connection::{ConnectOptions as _, Connection as SqlxConnection},
21 row::Row,
22 sql_str::AssertSqlSafe,
23};
24use sqlx_mysql::{MySqlArguments, MySqlConnectOptions, MySqlConnection, MySqlDatabaseError};
25use std::{borrow::Cow, cell::Cell, sync::Arc};
26use toasty_core::{
27 Result, Schema,
28 driver::{
29 Capability, ConnectContext, ConnectionUrl, Driver, ExecResponse, Operation, QueryLogConfig,
30 log::QueryLog,
31 operation::{RawSqlRet, Transaction, TransactionMode},
32 },
33 schema::{
34 db::{self, Migration, Table},
35 diff,
36 },
37 stmt::{self, ValueRecord},
38};
39use toasty_sql::{self as sql};
40
41enum SqlReturn {
42 Count,
43 LastInsertId(stmt::Type),
44 Infer,
45 Types(Vec<stmt::Type>),
46}
47
48fn classify_mysql_error(e: sqlx_core::Error) -> toasty_core::Error {
54 match e {
55 error @ (sqlx_core::Error::Io(_)
56 | sqlx_core::Error::Protocol(_)
57 | sqlx_core::Error::WorkerCrashed) => toasty_core::Error::connection_lost(error),
58 sqlx_core::Error::Database(database_error) => {
59 let mysql_error = database_error
60 .try_downcast_ref::<MySqlDatabaseError>()
61 .expect("SQLx returned a non-MySQL database error from a MySQL connection");
62 let number = mysql_error.number();
63 let message = mysql_error.message().to_owned();
64
65 match number {
66 1213 => toasty_core::Error::serialization_failure(message),
67 1792 => toasty_core::Error::read_only_transaction(message),
68 _ => toasty_core::Error::driver_operation_failed(sqlx_core::Error::Database(
69 database_error,
70 )),
71 }
72 }
73 other => toasty_core::Error::driver_operation_failed(other),
74 }
75}
76
77fn record_mysql_err(valid: &Cell<bool>, e: sqlx_core::Error) -> toasty_core::Error {
79 let err = classify_mysql_error(e);
80 if err.is_connection_lost() {
81 valid.set(false);
82 }
83 err
84}
85
86#[derive(Debug)]
96pub struct MySQL {
97 url: String,
98 opts: MySqlConnectOptions,
99}
100
101impl MySQL {
102 pub fn new(url: impl Into<String>) -> Result<Self> {
107 let url_str = url.into();
108 let url = ConnectionUrl::parse(&url_str)?;
109
110 if !url.has_scheme("mysql") {
111 return Err(toasty_core::Error::invalid_connection_url(format!(
112 "connection url does not have a `mysql` scheme; url={}",
113 url.as_str()
114 )));
115 }
116
117 url.host()?.ok_or_else(|| {
118 toasty_core::Error::invalid_connection_url(format!(
119 "missing host in connection URL; url={}",
120 url.as_str()
121 ))
122 })?;
123
124 if url.path().is_empty() {
125 return Err(toasty_core::Error::invalid_connection_url(format!(
126 "no database specified - missing path in connection URL; url={}",
127 url.as_str()
128 )));
129 }
130
131 let opts = url
132 .as_str()
133 .parse::<MySqlConnectOptions>()
134 .map_err(toasty_core::Error::driver_operation_failed)?
135 .disable_statement_logging();
136
137 Ok(Self { url: url_str, opts })
138 }
139}
140
141#[async_trait]
142impl Driver for MySQL {
143 fn url(&self) -> Cow<'_, str> {
144 Cow::Borrowed(&self.url)
145 }
146
147 fn capability(&self) -> &'static Capability {
148 &Capability::MYSQL
149 }
150
151 async fn connect(
152 &self,
153 cx: &ConnectContext,
154 ) -> Result<Box<dyn toasty_core::driver::Connection>> {
155 let conn = MySqlConnection::connect_with(&self.opts)
156 .await
157 .map_err(classify_mysql_error)?;
158 let mut connection = Connection::new(conn);
159 connection.query_log = cx.query_log;
160 Ok(Box::new(connection))
161 }
162
163 fn generate_migration(&self, schema_diff: &diff::Schema<'_>) -> Migration {
164 let statements = sql::MigrationStatement::from_diff(schema_diff, &Capability::MYSQL);
165
166 let sql_strings: Vec<String> = statements
167 .iter()
168 .map(|stmt| sql::Serializer::mysql(stmt.schema()).serialize(stmt.statement()))
169 .collect();
170
171 Migration::new_sql_with_breakpoints(&sql_strings)
172 }
173
174 async fn reset_db(&self) -> Result<()> {
175 let mut conn = MySqlConnection::connect_with(&self.opts)
176 .await
177 .map_err(classify_mysql_error)?;
178 let dbname = self.opts.get_database().ok_or_else(|| {
179 toasty_core::Error::invalid_connection_url("no database name configured")
180 })?;
181
182 let dbname = format!("`{}`", dbname.replace('`', "``"));
183
184 sqlx_core::raw_sql::raw_sql(AssertSqlSafe(format!("DROP DATABASE IF EXISTS {dbname}")))
185 .execute(&mut conn)
186 .await
187 .map_err(classify_mysql_error)?;
188 sqlx_core::raw_sql::raw_sql(AssertSqlSafe(format!("CREATE DATABASE {dbname}")))
189 .execute(&mut conn)
190 .await
191 .map_err(classify_mysql_error)?;
192 sqlx_core::raw_sql::raw_sql(AssertSqlSafe(format!("USE {dbname}")))
193 .execute(&mut conn)
194 .await
195 .map_err(classify_mysql_error)?;
196
197 Ok(())
198 }
199}
200
201#[derive(Debug)]
203pub struct Connection {
204 conn: MySqlConnection,
205 valid: Cell<bool>,
208 query_log: QueryLogConfig,
209}
210
211impl Connection {
212 pub fn new(conn: MySqlConnection) -> Self {
214 Self {
215 conn,
216 valid: Cell::new(true),
217 query_log: QueryLogConfig::default(),
218 }
219 }
220
221 async fn exec_sql(
222 &mut self,
223 sql_as_str: &str,
224 args: MySqlArguments,
225 ret: SqlReturn,
226 log: &mut QueryLog<'_>,
227 ) -> Result<ExecResponse> {
228 if matches!(ret, SqlReturn::Count | SqlReturn::LastInsertId(_)) {
229 let result = sqlx_core::query::query_with(AssertSqlSafe(sql_as_str), args)
230 .execute(&mut self.conn)
231 .await
232 .map_err(|e| record_mysql_err(&self.valid, e))?;
233
234 if let SqlReturn::LastInsertId(ty) = ret {
235 let id = ty.cast(&(), stmt::Value::U64(result.last_insert_id()))?;
236 log.rows(1);
237 return Ok(ExecResponse::value_stream(stmt::ValueStream::from_vec(
238 vec![ValueRecord::from_vec(vec![id]).into()],
239 )));
240 }
241
242 return Ok(ExecResponse::count(result.rows_affected()));
243 }
244
245 let rows = sqlx_core::query::query_with(AssertSqlSafe(sql_as_str), args)
246 .fetch_all(&mut self.conn)
247 .await
248 .map_err(|e| record_mysql_err(&self.valid, e))?;
249
250 log.rows(rows.len() as u64);
251 let mut records = Vec::with_capacity(rows.len());
252
253 for row in rows {
254 let mut values = Vec::with_capacity(row.len());
255
256 match &ret {
257 SqlReturn::Count | SqlReturn::LastInsertId(_) => unreachable!(),
258 SqlReturn::Infer => {
259 for i in 0..row.len() {
260 let column = row.column(i);
261 values.push(
262 Value::from_sql_infer(i, &row, column)
263 .map_err(|e| record_mysql_err(&self.valid, e))?
264 .into_inner(),
265 );
266 }
267 }
268 SqlReturn::Types(returning) => {
269 assert_eq!(
270 row.len(),
271 returning.len(),
272 "row={row:#?}; returning={returning:#?}"
273 );
274
275 for i in 0..row.len() {
276 let column = row.column(i);
277 values.push(
278 Value::from_sql(i, &row, column, &returning[i])
279 .map_err(|e| record_mysql_err(&self.valid, e))?
280 .into_inner(),
281 );
282 }
283 }
284 }
285
286 records.push(Ok(ValueRecord::from_vec(values)));
287 }
288
289 Ok(ExecResponse::value_stream(stmt::ValueStream::from_iter(
290 records.into_iter(),
291 )))
292 }
293
294 pub async fn create_table(&mut self, schema: &db::Schema, table: &Table) -> Result<()> {
296 let serializer = sql::Serializer::mysql(schema);
297 let statement =
298 serializer.serialize(&sql::Statement::create_table(table, &Capability::MYSQL));
299
300 sqlx_core::query::query(AssertSqlSafe(statement))
301 .execute(&mut self.conn)
302 .await
303 .map_err(|e| record_mysql_err(&self.valid, e))?;
304
305 for index in &table.indices {
306 if index.primary_key {
307 continue;
308 }
309
310 let statement = serializer.serialize(&sql::Statement::create_index(index));
311 sqlx_core::query::query(AssertSqlSafe(statement))
312 .execute(&mut self.conn)
313 .await
314 .map_err(|e| record_mysql_err(&self.valid, e))?;
315 }
316
317 Ok(())
318 }
319}
320
321impl From<MySqlConnection> for Connection {
322 fn from(conn: MySqlConnection) -> Self {
323 Self::new(conn)
324 }
325}
326
327#[async_trait]
328impl toasty_core::driver::Connection for Connection {
329 async fn exec(&mut self, schema: &Arc<Schema>, op: Operation) -> Result<ExecResponse> {
330 tracing::trace!(driver = "mysql", op = %op.name(), "driver exec");
331
332 let (sql, typed_params, ret) = match op {
333 Operation::Insert(op) => {
334 let ret = match op.ret {
335 Some(types) => {
336 let [ty] = &types[..] else {
337 return Err(toasty_core::Error::invalid_result(format!(
338 "MySQL insert ID result requires one type, got {types:?}"
339 )));
340 };
341 SqlReturn::LastInsertId(ty.clone())
342 }
343 None => SqlReturn::Count,
344 };
345 (sql::Statement::from(op.stmt), op.params, ret)
346 }
347 Operation::QuerySql(op) => {
348 let ret = match op.ret {
349 Some(types) => SqlReturn::Types(types),
350 None => SqlReturn::Count,
351 };
352 (sql::Statement::from(op.stmt), op.params, ret)
353 }
354 Operation::RawSql(op) => {
355 let ret = match op.ret {
356 RawSqlRet::None => SqlReturn::Count,
357 RawSqlRet::Infer => SqlReturn::Infer,
358 RawSqlRet::Types(types) => SqlReturn::Types(types),
359 };
360 let mut log = QueryLog::sql(
361 &self.query_log,
362 "mysql",
363 &op.sql,
364 op.params.iter().map(|tv| &tv.value),
365 );
366 let mut args = MySqlArguments::default();
367 for param in op.params {
368 Value::from(param.value)
369 .add_to(&mut args)
370 .map_err(|e| record_mysql_err(&self.valid, e))?;
371 }
372 let result = self.exec_sql(&op.sql, args, ret, &mut log).await;
373 log.finish(&result);
374 return result;
375 }
376 Operation::Transaction(op) => {
377 if let Transaction::Start {
378 mode: mode @ (TransactionMode::Immediate | TransactionMode::Exclusive),
379 ..
380 } = &op
381 {
382 return Err(toasty_core::Error::unsupported_feature(format!(
383 "MySQL does not support TransactionMode::{mode:?}"
384 )));
385 }
386 let statement = sql::Serializer::mysql(&schema.db).serialize_transaction(&op);
387 sqlx_core::raw_sql::raw_sql(AssertSqlSafe(statement))
388 .execute(&mut self.conn)
389 .await
390 .map_err(|e| record_mysql_err(&self.valid, e))?;
391 return Ok(ExecResponse::count(0));
392 }
393 op => todo!("op={op:#?}"),
394 };
395
396 let (sql_as_str, arg_order) =
397 sql::Serializer::mysql(&schema.db).serialize_with_arg_order(&sql);
398
399 let mut log = QueryLog::sql(
400 &self.query_log,
401 "mysql",
402 &sql_as_str,
403 arg_order.iter().map(|&pos| &typed_params[pos].value),
404 );
405
406 let mut remaining = vec![0usize; typed_params.len()];
409 for &pos in &arg_order {
410 remaining[pos] += 1;
411 }
412 let mut values = typed_params
413 .into_iter()
414 .map(|param| Some(param.value))
415 .collect::<Vec<_>>();
416 let mut args = MySqlArguments::default();
417 for pos in arg_order {
418 remaining[pos] -= 1;
419 let value = if remaining[pos] == 0 {
420 values[pos].take().expect("MySQL parameter already moved")
421 } else {
422 values[pos]
423 .as_ref()
424 .expect("MySQL parameter missing")
425 .clone()
426 };
427 Value::from(value)
428 .add_to(&mut args)
429 .map_err(|e| record_mysql_err(&self.valid, e))?;
430 }
431
432 let result = self.exec_sql(&sql_as_str, args, ret, &mut log).await;
433 log.finish(&result);
434 result
435 }
436
437 async fn push_schema(&mut self, schema: &Schema) -> Result<()> {
438 for table in &schema.db.tables {
439 tracing::debug!(table = %table.name, "creating table");
440 self.create_table(&schema.db, table).await?;
441 }
442 Ok(())
443 }
444
445 async fn applied_migrations(
446 &mut self,
447 ) -> Result<Vec<toasty_core::schema::db::AppliedMigration>> {
448 sqlx_core::query::query(
449 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
450 id BIGINT UNSIGNED PRIMARY KEY,
451 name TEXT NOT NULL,
452 applied_at TIMESTAMP NOT NULL
453 )",
454 )
455 .execute(&mut self.conn)
456 .await
457 .map_err(|e| record_mysql_err(&self.valid, e))?;
458
459 let ids = sqlx_core::query_scalar::query_scalar::<sqlx_mysql::MySql, u64>(
460 "SELECT id FROM __toasty_migrations ORDER BY applied_at",
461 )
462 .fetch_all(&mut self.conn)
463 .await
464 .map_err(|e| record_mysql_err(&self.valid, e))?;
465
466 Ok(ids
467 .into_iter()
468 .map(toasty_core::schema::db::AppliedMigration::new)
469 .collect())
470 }
471
472 async fn apply_migration(
473 &mut self,
474 id: u64,
475 name: &str,
476 migration: &toasty_core::schema::db::Migration,
477 ) -> Result<()> {
478 tracing::info!(id, name, "applying migration");
479 sqlx_core::query::query(
480 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
481 id BIGINT UNSIGNED PRIMARY KEY,
482 name TEXT NOT NULL,
483 applied_at TIMESTAMP NOT NULL
484 )",
485 )
486 .execute(&mut self.conn)
487 .await
488 .map_err(|e| record_mysql_err(&self.valid, e))?;
489
490 let mut transaction = self
491 .conn
492 .begin()
493 .await
494 .map_err(|e| record_mysql_err(&self.valid, e))?;
495
496 for statement in migration.statements() {
497 if let Err(error) = sqlx_core::raw_sql::raw_sql(AssertSqlSafe(statement))
498 .execute(&mut *transaction)
499 .await
500 {
501 let error = record_mysql_err(&self.valid, error);
502 transaction
503 .rollback()
504 .await
505 .map_err(|e| record_mysql_err(&self.valid, e))?;
506 return Err(error);
507 }
508 }
509
510 if let Err(error) = sqlx_core::query::query(
511 "INSERT INTO __toasty_migrations (id, name, applied_at) VALUES (?, ?, NOW())",
512 )
513 .bind(id)
514 .bind(name)
515 .execute(&mut *transaction)
516 .await
517 {
518 let error = record_mysql_err(&self.valid, error);
519 transaction
520 .rollback()
521 .await
522 .map_err(|e| record_mysql_err(&self.valid, e))?;
523 return Err(error);
524 }
525
526 transaction
527 .commit()
528 .await
529 .map_err(|e| record_mysql_err(&self.valid, e))?;
530 Ok(())
531 }
532
533 fn is_valid(&self) -> bool {
534 self.valid.get()
535 }
536
537 async fn ping(&mut self) -> Result<()> {
538 match self.conn.ping().await {
539 Ok(()) => Ok(()),
540 Err(error) => {
541 self.valid.set(false);
542 Err(toasty_core::Error::connection_lost(error))
543 }
544 }
545 }
546}