1use super::auth::{self, FastAuth};
4use super::protocol::{self, Buffer, Column, OkPacket, Packet, ServerError};
5use super::types;
6use crate::config::DatabaseConfig;
7use crate::driver::{BoxFuture, Driver, DriverConnection, QueryResult};
8use crate::row::{Columns, Row};
9use crate::value::Value;
10use rustlavel_core::events::Event;
11use rustlavel_core::{Error, Result};
12use std::sync::Arc;
13use std::time::Instant;
14use tokio::net::TcpStream;
15
16pub const DEFAULT_PORT: u16 = 3306;
19
20pub const DRIVER_NAME: &str = "mysql";
22
23pub struct MySqlConnection {
24 stream: crate::tls::DbStream,
25 buffer: Vec<u8>,
27 config: DatabaseConfig,
28 connection_id: u32,
29 server_version: String,
30 capabilities: u32,
33 status: u16,
34 sequence: u8,
38 broken: bool,
40}
41
42impl MySqlConnection {
43 pub async fn connect(config: &DatabaseConfig) -> Result<MySqlConnection> {
45 let address = format!("{}:{}", config.host, config.port);
46
47 let stream = tokio::time::timeout(config.connect_timeout, TcpStream::connect(&address))
48 .await
49 .map_err(|_| {
50 Error::msg(format!(
53 "MySQL error 2003: timed out connecting to {address}. Is MySQL running and \
54 reachable?"
55 ))
56 })?
57 .map_err(|e| {
58 Error::msg(format!(
59 "MySQL error 2003: cannot connect to {address}: {e}\n \
60 Check that the server is running and that DATABASE_URL points at it ({}).",
61 config.redacted_url()
62 ))
63 })?;
64
65 let _ = stream.set_nodelay(true);
66
67 let mut connection = MySqlConnection {
68 stream: crate::tls::DbStream::Plain(stream),
69 buffer: Vec::with_capacity(8 * 1024),
70 config: config.clone(),
71 connection_id: 0,
72 server_version: String::new(),
73 capabilities: 0,
74 status: 0,
75 sequence: 0,
76 broken: false,
77 };
78
79 connection.handshake().await?;
80 Ok(connection)
81 }
82
83 pub fn is_broken(&self) -> bool {
84 self.broken
85 }
86
87 pub fn in_transaction(&self) -> bool {
94 self.status & protocol::SERVER_STATUS_IN_TRANS != 0
95 }
96
97 pub fn server_version(&self) -> &str {
99 &self.server_version
100 }
101
102 pub fn connection_id(&self) -> u32 {
104 self.connection_id
105 }
106
107 pub fn capabilities(&self) -> u32 {
110 self.capabilities
111 }
112
113 pub fn is_encrypted(&self) -> bool {
117 self.stream.is_encrypted()
118 }
119
120 async fn negotiate_tls(&mut self, capabilities: u32, server: u32) -> Result<u32> {
134 let mode = self.config.tls_mode;
135 if !mode.wants_tls() {
136 return Ok(capabilities);
137 }
138
139 if server & protocol::CLIENT_SSL == 0 {
140 if mode.demands_tls() {
141 self.broken = true;
142 return Err(Error::msg(format!(
143 "sslmode is `{mode}` but this MySQL does not offer TLS: it did not advertise CLIENT_SSL in its handshake. The server was built or configured without a certificate — set `ssl_cert` and `ssl_key` on it, or set sslmode=prefer to accept a connection in clear text."
144 )));
145 }
146 return Ok(capabilities);
148 }
149
150 let mut request = Buffer::new();
151 request.ssl_request(capabilities);
152 self.write_packet(request).await?;
153
154 let plain = self.stream.take_plain()?;
155 let encrypted = crate::tls::upgrade(plain, &self.config.host, &self.config).await?;
156 self.stream = crate::tls::DbStream::Tls(Box::new(encrypted));
157
158 Ok(capabilities | protocol::CLIENT_SSL)
159 }
160
161 async fn handshake(&mut self) -> Result<()> {
162 let greeting = self.read_packet().await?;
163 if protocol::is_err(&greeting) {
164 self.broken = true;
167 return Err(server_error(protocol::parse_err(&greeting)?, &self.config, None));
168 }
169
170 let handshake = protocol::parse_handshake(&greeting)?;
171 self.connection_id = handshake.connection_id;
172 self.server_version = handshake.server_version.clone();
173
174 let mut capabilities = protocol::CLIENT_CAPABILITIES & handshake.capabilities;
175 capabilities |= protocol::CLIENT_PROTOCOL_41 | protocol::CLIENT_SECURE_CONNECTION;
178 if !self.config.database.is_empty() {
179 capabilities |= protocol::CLIENT_CONNECT_WITH_DB;
180 }
181 self.capabilities = self.negotiate_tls(capabilities, handshake.capabilities).await?;
185 let capabilities = self.capabilities;
186
187 let mut plugin = if handshake.auth_plugin.is_empty() {
190 auth::MYSQL_NATIVE_PASSWORD.to_string()
191 } else {
192 handshake.auth_plugin.clone()
193 };
194
195 if !auth::is_supported(&plugin) {
196 self.broken = true;
197 return Err(auth::insecure_plugin_error(&plugin));
198 }
199
200 let response = auth::respond(&plugin, &self.config.password, &handshake.scramble)?;
201
202 let mut reply = Buffer::new();
203 reply.handshake_response(
204 capabilities,
205 &self.config.user,
206 &response,
207 Some(&self.config.database),
208 &plugin,
209 &[
210 ("_client_name", "rustlavel"),
211 ("program_name", self.config.application_name.as_str()),
212 ],
213 );
214 self.write_packet(reply).await?;
215
216 loop {
219 let payload = self.read_packet().await?;
220
221 match Packet::parse(&payload)? {
222 Packet::Ok(ok) => {
223 self.status = ok.status;
224 return Ok(());
225 }
226 Packet::Err(error) => {
227 self.broken = true;
228 return Err(authentication_error(error, &self.config));
229 }
230 Packet::AuthSwitch { plugin: wanted, data } => {
231 if !auth::is_supported(&wanted) {
232 self.broken = true;
233 return Err(auth::insecure_plugin_error(&wanted));
234 }
235 plugin = wanted;
238 let response = auth::respond(&plugin, &self.config.password, &data)?;
239 let mut reply = Buffer::new();
240 reply.auth_response(&response);
241 self.write_packet(reply).await?;
242 }
243 Packet::AuthMoreData(data) if plugin == auth::CACHING_SHA2_PASSWORD => {
244 match auth::fast_auth_status(&data)? {
245 FastAuth::Succeeded => continue,
247 FastAuth::FullAuthRequired if self.stream.is_encrypted() => {
255 let mut reply = Buffer::new();
256 reply.auth_response(&auth::cleartext_password(
257 &self.config.password,
258 ));
259 self.write_packet(reply).await?;
260 }
261 FastAuth::FullAuthRequired => {
262 self.broken = true;
263 return Err(auth::full_auth_error(
264 &self.config.user,
265 &format!("{}:{}", self.config.host, self.config.port),
266 ));
267 }
268 }
269 }
270 Packet::AuthMoreData(_) | Packet::Eof(_) | Packet::Other(_) => {
271 self.broken = true;
272 return Err(Error::Protocol(format!(
273 "unexpected packet during {plugin} authentication"
274 )));
275 }
276 }
277 }
278 }
279
280 pub async fn simple_query(&mut self, sql: &str) -> Result<QueryResult> {
285 let started = Instant::now();
286
287 let mut command = Buffer::new();
288 command.com_query(sql);
289 self.sequence = 0;
290 let result = match self.write_packet(command).await {
291 Ok(()) => self.read_result_set(sql, false).await,
292 Err(e) => Err(e),
293 };
294
295 self.record(sql, &[], started, &result);
296 result
297 }
298
299 pub async fn query(&mut self, sql: &str, params: &[Value]) -> Result<QueryResult> {
307 let started = Instant::now();
308 let result = self.prepared(sql, params).await;
309 self.record(sql, params, started, &result);
310 result
311 }
312
313 async fn prepared(&mut self, sql: &str, params: &[Value]) -> Result<QueryResult> {
314 let statement = self.prepare(sql).await?;
315
316 if statement.params as usize != params.len() {
317 self.close_statement(statement.statement_id).await?;
318 return Err(Error::msg(format!(
319 "the statement takes {} parameter(s) but {} were bound.\n SQL: {sql}",
320 statement.params,
321 params.len()
322 )));
323 }
324
325 let mut command = Buffer::new();
326 command.com_stmt_execute(statement.statement_id, params);
327 self.sequence = 0;
328 let result = match self.write_packet(command).await {
329 Ok(()) => self.read_result_set(sql, true).await,
330 Err(e) => Err(e),
331 };
332
333 self.close_statement(statement.statement_id).await?;
336 result
337 }
338
339 async fn prepare(&mut self, sql: &str) -> Result<protocol::PrepareOk> {
341 let mut command = Buffer::new();
342 command.com_stmt_prepare(sql);
343 self.sequence = 0;
344 self.write_packet(command).await?;
345
346 let payload = self.read_packet().await?;
347 if protocol::is_err(&payload) {
348 return Err(server_error(protocol::parse_err(&payload)?, &self.config, Some(sql)));
349 }
350
351 let prepared = protocol::parse_prepare_ok(&payload)?;
352
353 if prepared.params > 0 {
358 self.skip_metadata(prepared.params as usize).await?;
359 }
360 if prepared.columns > 0 {
361 self.skip_metadata(prepared.columns as usize).await?;
362 }
363
364 Ok(prepared)
365 }
366
367 async fn skip_metadata(&mut self, count: usize) -> Result<()> {
368 for _ in 0..count {
369 self.read_packet().await?;
370 }
371 let payload = self.read_packet().await?;
374 if !protocol::is_eof(&payload) {
375 return Err(Error::Protocol(
376 "expected an EOF after a metadata block from the server".into(),
377 ));
378 }
379 Ok(())
380 }
381
382 async fn close_statement(&mut self, statement_id: u32) -> Result<()> {
383 let mut command = Buffer::new();
384 command.com_stmt_close(statement_id);
385 self.sequence = 0;
386 self.write_packet(command).await
388 }
389
390 async fn read_result_set(&mut self, sql: &str, binary: bool) -> Result<QueryResult> {
395 let mut result = QueryResult::default();
396 let mut first = true;
397
398 loop {
399 let payload = self.read_packet().await?;
400
401 if protocol::is_err(&payload) {
402 return Err(server_error(protocol::parse_err(&payload)?, &self.config, Some(sql)));
403 }
404
405 if payload.first() == Some(&0x00) {
406 let ok = protocol::parse_ok(&payload)?;
409 self.status = ok.status;
410 if first {
411 apply_ok(&mut result, &ok);
412 }
413 if ok.status & protocol::SERVER_MORE_RESULTS_EXISTS != 0 {
414 first = false;
415 continue;
416 }
417 return Ok(result);
418 }
419
420 let count = protocol::Reader::new(&payload).lenenc_int()? as usize;
421 let columns = self.read_columns(count).await?;
422 let more = self.read_rows(&columns, binary, first.then_some(&mut result)).await?;
423
424 if !more {
425 return Ok(result);
426 }
427 first = false;
428 }
429 }
430
431 async fn read_columns(&mut self, count: usize) -> Result<Vec<Column>> {
433 let mut columns = Vec::with_capacity(count);
434 for _ in 0..count {
435 let payload = self.read_packet().await?;
436 columns.push(protocol::parse_column(&payload)?);
437 }
438
439 let payload = self.read_packet().await?;
440 if !protocol::is_eof(&payload) {
441 return Err(Error::Protocol(
442 "expected an EOF after the column definitions".into(),
443 ));
444 }
445 Ok(columns)
446 }
447
448 async fn read_rows(
455 &mut self,
456 columns: &[Column],
457 binary: bool,
458 into: Option<&mut QueryResult>,
459 ) -> Result<bool> {
460 let names: Columns = Arc::new(columns.iter().map(|c| c.name.clone()).collect());
461 let mut rows = Vec::new();
462
463 let status = loop {
464 let payload = self.read_packet().await?;
465
466 if protocol::is_err(&payload) {
467 return Err(server_error(protocol::parse_err(&payload)?, &self.config, None));
468 }
469 if protocol::is_eof(&payload) {
470 let eof = protocol::parse_eof(&payload)?;
471 self.status = eof.status;
472 break eof.status;
473 }
474
475 let values = if binary {
476 decode_binary_row(&payload, columns)?
477 } else {
478 protocol::parse_text_row(&payload, columns.len())?
479 .iter()
480 .zip(columns)
481 .map(|(raw, column)| types::decode_text(column, raw.as_deref()))
482 .collect()
483 };
484 rows.push(Row::new(Arc::clone(&names), values));
485 };
486
487 if let Some(result) = into {
488 result.affected = rows.len() as u64;
491 result.rows = rows;
492 }
493
494 Ok(status & protocol::SERVER_MORE_RESULTS_EXISTS != 0)
495 }
496
497 pub async fn ping(&mut self) -> Result<()> {
499 let mut command = Buffer::new();
500 command.com_ping();
501 self.sequence = 0;
502 self.write_packet(command).await?;
503
504 let payload = self.read_packet().await?;
505 match Packet::parse(&payload)? {
506 Packet::Ok(ok) => {
507 self.status = ok.status;
508 Ok(())
509 }
510 Packet::Err(error) => Err(server_error(error, &self.config, None)),
511 _ => Err(Error::Protocol("the server answered a ping with something else".into())),
512 }
513 }
514
515 fn record(&self, sql: &str, params: &[Value], started: Instant, result: &Result<QueryResult>) {
517 let elapsed = started.elapsed();
518
519 if rustlavel_core::events::has_subscribers() {
520 let bindings = if crate::postgres::connection::log_bindings() {
524 params.iter().map(Value::to_display).collect::<Vec<_>>().join(", ")
525 } else {
526 format!("{} value(s) hidden", params.len())
527 };
528 Event::new("db.query")
529 .with("sql", sql)
530 .with("bindings", bindings)
531 .with("rows", result.as_ref().map(|r| r.rows.len()).unwrap_or(0))
532 .with("ok", result.is_ok())
533 .took(elapsed)
534 .dispatch();
535 }
536
537 rustlavel_core::debug!("db: {sql} ({:.1}ms)", elapsed.as_secs_f64() * 1000.0);
538 }
539
540 async fn write_packet(&mut self, buffer: Buffer) -> Result<()> {
542 let (bytes, next) = protocol::frame(&buffer.into_bytes(), self.sequence);
543 self.sequence = next;
544
545 if let Err(e) = self.stream.write_all(&bytes).await {
546 self.broken = true;
547 return Err(Error::Io(e));
548 }
549 if let Err(e) = self.stream.flush().await {
550 self.broken = true;
551 return Err(Error::Io(e));
552 }
553 Ok(())
554 }
555
556 async fn read_packet(&mut self) -> Result<Vec<u8>> {
559 let mut payload = Vec::new();
560
561 loop {
562 self.fill_to(4).await?;
563 let length =
564 u32::from_le_bytes([self.buffer[0], self.buffer[1], self.buffer[2], 0]) as usize;
565 let sequence = self.buffer[3];
566
567 self.fill_to(4 + length).await?;
568 payload.extend_from_slice(&self.buffer[4..4 + length]);
569 self.buffer.drain(..4 + length);
570 self.sequence = sequence.wrapping_add(1);
571
572 if length < protocol::MAX_PAYLOAD {
575 return Ok(payload);
576 }
577 }
578 }
579
580 async fn fill_to(&mut self, wanted: usize) -> Result<()> {
582 while self.buffer.len() < wanted {
583 let mut chunk = [0u8; 8192];
584 let read = match self.stream.read(&mut chunk).await {
585 Ok(read) => read,
586 Err(e) => {
587 self.broken = true;
588 return Err(Error::Io(e));
589 }
590 };
591 if read == 0 {
592 self.broken = true;
593 return Err(Error::Protocol(
594 "the database closed the connection unexpectedly".into(),
595 ));
596 }
597 self.buffer.extend_from_slice(&chunk[..read]);
598 }
599 Ok(())
600 }
601
602 pub async fn close(mut self) {
604 let mut command = Buffer::new();
605 command.com_quit();
606 self.sequence = 0;
607 let _ = self.write_packet(command).await;
608 let _ = self.stream.shutdown().await;
609 }
610}
611
612fn server_error(error: ServerError, config: &DatabaseConfig, sql: Option<&str>) -> Error {
614 let advice = match error.code {
615 1049 => Some(format!(
616 "Database `{}` does not exist. Create it, or point DATABASE_URL at one that does.",
617 config.database
618 )),
619 1044 => Some(format!(
620 "User `{}` has no rights on `{}`. Grant them, or connect as a user who has.",
621 config.user, config.database
622 )),
623 1146 => Some("The table is missing. Have the migrations been run?".to_string()),
624 _ => None,
625 };
626
627 let base = error.into_error(sql);
628 match advice {
629 Some(advice) => Error::msg(format!("{base}\n {advice}")),
630 None => base,
631 }
632}
633
634fn authentication_error(error: ServerError, config: &DatabaseConfig) -> Error {
636 let advice = match error.code {
637 1045 => Some(format!(
638 "The password for `{}` was rejected. Check DATABASE_URL in your .env.",
639 config.user
640 )),
641 1049 => Some(format!(
642 "Database `{}` does not exist. Create it, or point DATABASE_URL at one that does.",
643 config.database
644 )),
645 1130 | 1698 => Some(format!(
646 "The server will not let `{}` in from this host. Check the account's host pattern \
647 and its authentication plugin.",
648 config.user
649 )),
650 1040 | 1203 => Some(
651 "The server is out of connections. Lower max_connections in DATABASE_URL, or raise \
652 the server's."
653 .to_string(),
654 ),
655 _ => None,
656 };
657
658 let base = error.into_error(None);
659 match advice {
660 Some(advice) => Error::msg(format!("{base}\n {advice}")),
661 None => base,
662 }
663}
664
665fn decode_binary_row(payload: &[u8], columns: &[Column]) -> Result<Vec<Value>> {
672 let mut reader = protocol::Reader::new(payload);
673 reader.skip(1)?; let bitmap = reader.take((columns.len() + 2).div_ceil(8))?;
676 let mut values = Vec::with_capacity(columns.len());
677
678 for (index, column) in columns.iter().enumerate() {
679 let bit = index + 2;
680 if bitmap[bit / 8] & (1 << (bit % 8)) != 0 {
681 values.push(Value::Null);
682 } else {
683 values.push(types::decode_binary(column, &mut reader)?);
684 }
685 }
686
687 Ok(values)
688}
689
690fn apply_ok(result: &mut QueryResult, ok: &OkPacket) {
691 result.affected = ok.affected_rows;
692 result.last_insert_id = (ok.last_insert_id != 0).then_some(ok.last_insert_id as i64);
695}
696
697pub struct MySqlDriver {
699 config: DatabaseConfig,
700 dialect: Arc<dyn crate::dialect::Dialect>,
701}
702
703impl MySqlDriver {
704 pub fn new(mut config: DatabaseConfig) -> Self {
705 config.driver = DRIVER_NAME.into();
710 MySqlDriver { config, dialect: Arc::new(crate::dialect::MySql) }
711 }
712
713 pub fn from_url(url: &str) -> Result<Self> {
715 let config = DatabaseConfig::from_url(url)?;
716
717 if config.driver != DRIVER_NAME {
720 return Err(Error::msg(format!(
721 "`{url}` is a {} URL, not a MySQL one. Expected \
722 mysql://user:password@host:port/database",
723 config.driver
724 )));
725 }
726
727 Ok(MySqlDriver::new(config))
728 }
729
730 pub fn config(&self) -> &DatabaseConfig {
731 &self.config
732 }
733}
734
735impl Driver for MySqlDriver {
736 fn generation(&self) -> u64 {
737 self.config.generation()
738 }
739
740 fn dialect(&self) -> Arc<dyn crate::dialect::Dialect> {
741 Arc::clone(&self.dialect)
742 }
743
744 fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
745 Box::pin(async move {
746 let connection = MySqlConnection::connect(&self.config.resolved()).await?;
747 Ok(Box::new(connection) as Box<dyn DriverConnection>)
748 })
749 }
750
751 fn describe(&self) -> String {
752 self.config.redacted_url()
753 }
754
755 fn max_connections(&self) -> usize {
756 self.config.max_connections
757 }
758}
759
760impl DriverConnection for MySqlConnection {
761 fn query<'a>(
762 &'a mut self,
763 sql: &'a str,
764 params: &'a [Value],
765 ) -> BoxFuture<'a, Result<QueryResult>> {
766 Box::pin(MySqlConnection::query(self, sql, params))
767 }
768
769 fn simple_query<'a>(&'a mut self, sql: &'a str) -> BoxFuture<'a, Result<QueryResult>> {
770 Box::pin(MySqlConnection::simple_query(self, sql))
771 }
772
773 fn is_broken(&self) -> bool {
774 MySqlConnection::is_broken(self)
775 }
776
777 fn in_transaction(&self) -> bool {
778 MySqlConnection::in_transaction(self)
779 }
780
781 fn close(self: Box<Self>) -> BoxFuture<'static, ()> {
782 Box::pin(async move { MySqlConnection::close(*self).await })
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use crate::mysql::protocol::{CHARSET_BINARY, CHARSET_UTF8MB4};
790
791 fn column(name: &str, column_type: u8) -> Column {
792 Column {
793 name: name.into(),
794 column_type,
795 charset: CHARSET_UTF8MB4 as u16,
796 ..Column::default()
797 }
798 }
799
800 #[test]
801 fn a_generated_key_of_zero_means_no_key_was_generated() {
802 let mut result = QueryResult::default();
803 apply_ok(&mut result, &OkPacket { affected_rows: 1, last_insert_id: 0, ..OkPacket::default() });
804 assert_eq!(result.last_insert_id, None);
805 assert_eq!(result.affected, 1);
806
807 apply_ok(&mut result, &OkPacket { affected_rows: 1, last_insert_id: 42, ..OkPacket::default() });
808 assert_eq!(result.last_insert_id, Some(42));
809 }
810
811 #[test]
812 fn a_binary_row_reads_its_nulls_from_a_bitmap_offset_by_two_bits() {
813 let columns = vec![column("id", types::LONGLONG), column("name", types::VAR_STRING)];
814
815 let mut payload = vec![0x00, 0b0000_1000];
817 payload.extend_from_slice(&7i64.to_le_bytes());
818
819 let values = decode_binary_row(&payload, &columns).unwrap();
820 assert_eq!(values, vec![Value::Int(7), Value::Null]);
821 }
822
823 #[test]
824 fn a_binary_row_with_no_nulls_decodes_every_column() {
825 let columns = vec![
826 column("id", types::LONGLONG),
827 column("name", types::VAR_STRING),
828 Column { charset: CHARSET_BINARY, ..column("blob", types::BLOB) },
829 ];
830
831 let mut payload = vec![0x00, 0x00];
832 payload.extend_from_slice(&7i64.to_le_bytes());
833 payload.extend_from_slice(b"\x03ada");
834 payload.extend_from_slice(b"\x02\xde\xad");
835
836 let values = decode_binary_row(&payload, &columns).unwrap();
837 assert_eq!(
838 values,
839 vec![Value::Int(7), Value::Text("ada".into()), Value::Bytes(vec![0xDE, 0xAD])]
840 );
841 }
842
843 #[test]
844 fn a_wide_row_needs_more_than_one_bitmap_byte() {
845 let columns: Vec<Column> =
848 (0..7).map(|i| column(&format!("c{i}"), types::TINY)).collect();
849
850 let mut payload = vec![0x00, 0b0000_0000, 0b0000_0001];
852 payload.extend_from_slice(&[1, 2, 3, 4, 5, 6]);
853
854 let values = decode_binary_row(&payload, &columns).unwrap();
855 assert_eq!(values.len(), 7);
856 assert_eq!(values[6], Value::Null);
857 assert_eq!(values[0], Value::Int(1));
858 }
859
860 #[test]
861 fn a_truncated_row_is_an_error_rather_than_a_panic() {
862 let columns = vec![column("id", types::LONGLONG)];
863 assert!(decode_binary_row(&[0x00], &columns).is_err());
864 assert!(decode_binary_row(&[0x00, 0x00, 1, 2], &columns).is_err());
865 }
866
867 #[test]
868 fn a_url_defaults_to_mysqls_port() {
869 let driver = MySqlDriver::from_url("mysql://localhost/blog").unwrap();
870
871 assert_eq!(driver.config().host, "localhost");
872 assert_eq!(driver.config().port, DEFAULT_PORT);
873 assert_eq!(driver.config().database, "blog");
874 assert_eq!(driver.config().driver, DRIVER_NAME);
875 }
876
877 #[test]
878 fn a_url_keeps_everything_it_states() {
879 let driver =
880 MySqlDriver::from_url("mysql://ada:hunter2@db.internal:3307/blog").unwrap();
881 let config = driver.config();
882
883 assert_eq!(config.user, "ada");
884 assert_eq!(config.password, "hunter2");
885 assert_eq!(config.host, "db.internal");
886 assert_eq!(config.port, 3307);
887 assert_eq!(config.database, "blog");
888 }
889
890 #[test]
891 fn mariadb_urls_are_accepted_too() {
892 let driver = MySqlDriver::from_url("mariadb://host/blog").unwrap();
894
895 assert_eq!(driver.config().database, "blog");
896 assert_eq!(driver.config().port, DEFAULT_PORT);
897 }
898
899 #[test]
900 fn rejects_a_url_that_points_at_another_database() {
901 let error = match MySqlDriver::from_url("postgres://host/blog") {
903 Err(error) => error.to_string(),
904 Ok(driver) => panic!("accepted a {} URL", driver.config().driver),
905 };
906 assert!(error.contains("not a MySQL one"), "{error}");
907 }
908
909 #[test]
910 fn a_hand_built_config_still_describes_itself_as_mysql() {
911 let driver = MySqlDriver::new(DatabaseConfig::default());
914 assert!(driver.describe().starts_with("mysql://"), "{}", driver.describe());
915 }
916
917 #[test]
918 fn the_driver_speaks_the_mysql_dialect_and_never_prints_the_password() {
919 let driver =
920 MySqlDriver::from_url("mysql://ada:hunter2@host/blog?max_connections=4").unwrap();
921
922 assert_eq!(driver.dialect().name(), "mysql");
923 assert!(driver.dialect().booleans_are_integers());
924 assert_eq!(driver.max_connections(), 4);
925
926 let shown = driver.describe();
927 assert!(!shown.contains("hunter2"), "{shown}");
928 assert!(shown.starts_with("mysql://ada:***@"), "{shown}");
929 }
930
931 #[test]
932 fn a_wrong_password_explains_where_to_look() {
933 let config = DatabaseConfig { user: "ada".into(), ..DatabaseConfig::default() };
934 let error = authentication_error(
935 ServerError {
936 code: 1045,
937 sql_state: "28000".into(),
938 message: "Access denied for user 'ada'@'localhost'".into(),
939 },
940 &config,
941 )
942 .to_string();
943
944 assert!(error.contains("1045"), "{error}");
945 assert!(error.contains("28000"), "{error}");
946 assert!(error.contains("Access denied"), "{error}");
947 assert!(error.contains("DATABASE_URL"), "{error}");
948 }
949
950 #[test]
951 fn an_unknown_database_names_the_one_that_was_asked_for() {
952 let config = DatabaseConfig { database: "blog".into(), ..DatabaseConfig::default() };
953 let error = server_error(
954 ServerError {
955 code: 1049,
956 sql_state: "42000".into(),
957 message: "Unknown database 'blog'".into(),
958 },
959 &config,
960 None,
961 )
962 .to_string();
963
964 assert!(error.contains("1049"), "{error}");
965 assert!(error.contains("`blog` does not exist"), "{error}");
966 }
967
968 #[test]
969 fn a_statement_error_carries_the_sql_that_caused_it() {
970 let error = server_error(
971 ServerError {
972 code: 1146,
973 sql_state: "42S02".into(),
974 message: "Table 'blog.nope' doesn't exist".into(),
975 },
976 &DatabaseConfig::default(),
977 Some("select * from nope"),
978 )
979 .to_string();
980
981 assert!(error.contains("SQL: select * from nope"), "{error}");
982 assert!(error.contains("migrations"), "{error}");
983 }
984
985 #[tokio::test]
986 async fn connecting_to_a_closed_port_names_the_server_and_the_client_error_code() {
987 let config = DatabaseConfig { port: 1, ..DatabaseConfig::default() };
988 let error = match MySqlConnection::connect(&config).await {
989 Err(error) => error.to_string(),
990 Ok(_) => panic!("nothing should be listening on port 1"),
991 };
992
993 assert!(error.contains("127.0.0.1:1"), "{error}");
994 assert!(error.contains("2003"), "{error}");
995 }
996
997 #[tokio::test]
998 async fn a_connection_that_never_opened_reports_no_transaction() {
999 let connection = offline(DatabaseConfig::default());
1001
1002 assert!(!connection.in_transaction());
1003 assert!(!connection.is_broken());
1004 assert_eq!(connection.connection_id(), 0);
1005 assert_eq!(connection.server_version(), "");
1006 }
1007
1008 fn offline(config: DatabaseConfig) -> MySqlConnection {
1011 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a free port");
1012 let address = listener.local_addr().expect("an address");
1013 let stream = std::net::TcpStream::connect(address).expect("a loopback connection");
1014 stream.set_nonblocking(true).expect("non-blocking");
1015
1016 MySqlConnection {
1017 stream: crate::tls::DbStream::Plain(
1018 TcpStream::from_std(stream).expect("a tokio stream"),
1019 ),
1020 buffer: Vec::new(),
1021 config,
1022 connection_id: 0,
1023 server_version: String::new(),
1024 capabilities: protocol::CLIENT_CAPABILITIES,
1025 status: 0,
1026 sequence: 0,
1027 broken: false,
1028 }
1029 }
1030}