fix(mysql): fall back to utf8 for servers without utf8mb4
This commit is contained in:
3 files changed
+183
-22
No files matched your search
@@ -1333,6 +1333,12 @@ impl MySqlTcpKeepaliveMode {
|
||||
|
||||
const MYSQL_GROUP_CONCAT_MAX_LEN: u64 = 1_048_576;
|
||||
|
||||
/// Charset used by the built-in `SET NAMES` setup when the connection config carries no
|
||||
/// `charset=` parameter, and the one written into MySQL connection URLs by the config layer.
|
||||
const MYSQL_DEFAULT_CHARSET: &str = "utf8mb4";
|
||||
/// Charset older (pre-5.5.3) servers do know; the fallback for [`MYSQL_DEFAULT_CHARSET`].
|
||||
const MYSQL_LEGACY_CHARSET: &str = "utf8";
|
||||
|
||||
impl MySqlSetupMode {
|
||||
fn group_concat_max_len_query(self, url: &str) -> Option<String> {
|
||||
match self {
|
||||
@@ -1369,29 +1375,101 @@ async fn verify_pool_connection_with_setup_fallback(
|
||||
eof_mode: MySqlEofMode,
|
||||
tcp_keepalive_mode: MySqlTcpKeepaliveMode,
|
||||
) -> Result<MySqlPool, String> {
|
||||
match verify_pool_connection(&pool, timeout).await {
|
||||
Ok(()) => Ok(pool),
|
||||
Err(err) => {
|
||||
let Some(fallback_mode) = mysql_group_concat_setup_fallback_mode(setup_mode, &err) else {
|
||||
return Err(err);
|
||||
};
|
||||
log::info!(
|
||||
"MySQL server rejected optional group_concat_max_len setup; retrying with {fallback_mode:?} mode"
|
||||
);
|
||||
let fallback_pool = create_pool(
|
||||
url,
|
||||
ca_cert_path,
|
||||
max_connections,
|
||||
idle_timeout_secs,
|
||||
setup_database,
|
||||
extra_setup_queries,
|
||||
fallback_mode,
|
||||
eof_mode,
|
||||
tcp_keepalive_mode,
|
||||
)?;
|
||||
verify_pool_connection(&fallback_pool, timeout).await.map(|_| fallback_pool)
|
||||
// The retry URL only changes when a fallback was actually applied, so a
|
||||
// connection that works on the first attempt never pays for the extra pool.
|
||||
let mut retry_url = url.to_string();
|
||||
let mut error = match verify_pool_connection(&pool, timeout).await {
|
||||
Ok(()) => return Ok(pool),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
if let Some(charset_url) = mysql_legacy_charset_fallback_url(&retry_url, &error) {
|
||||
log::info!("MySQL server does not support the built-in utf8mb4 charset; retrying with charset=utf8");
|
||||
let charset_pool = create_pool(
|
||||
&charset_url,
|
||||
ca_cert_path,
|
||||
max_connections,
|
||||
idle_timeout_secs,
|
||||
setup_database,
|
||||
extra_setup_queries,
|
||||
setup_mode,
|
||||
eof_mode,
|
||||
tcp_keepalive_mode,
|
||||
)?;
|
||||
match verify_pool_connection(&charset_pool, timeout).await {
|
||||
Ok(()) => return Ok(charset_pool),
|
||||
Err(charset_error) => {
|
||||
retry_url = charset_url;
|
||||
error = charset_error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(fallback_mode) = mysql_group_concat_setup_fallback_mode(setup_mode, &error) else {
|
||||
return Err(error);
|
||||
};
|
||||
log::info!("MySQL server rejected optional group_concat_max_len setup; retrying with {fallback_mode:?} mode");
|
||||
let fallback_pool = create_pool(
|
||||
&retry_url,
|
||||
ca_cert_path,
|
||||
max_connections,
|
||||
idle_timeout_secs,
|
||||
setup_database,
|
||||
extra_setup_queries,
|
||||
fallback_mode,
|
||||
eof_mode,
|
||||
tcp_keepalive_mode,
|
||||
)?;
|
||||
verify_pool_connection(&fallback_pool, timeout).await.map(|_| fallback_pool)
|
||||
}
|
||||
|
||||
/// MySQL older than 5.5.3 has no `utf8mb4` charset, so the built-in `SET NAMES utf8mb4`
|
||||
/// setup is rejected with `ERROR 1115 (42000): Unknown character set: 'utf8mb4'` and the
|
||||
/// whole connection fails (issue #9285, MySQL 5.1.73). Retry such a connection with the
|
||||
/// three-byte `utf8` charset those servers do know; newer servers keep `utf8mb4`.
|
||||
///
|
||||
/// `utf8mb4` is dbx's own built-in default — it is what `SET NAMES` uses when the
|
||||
/// connection config carries no `charset=` parameter, and the config layer writes it into
|
||||
/// the URL for every MySQL connection. So the retry only applies to that default: a
|
||||
/// connection that explicitly asks for another charset keeps it and reports the server
|
||||
/// error as-is.
|
||||
fn mysql_legacy_charset_fallback_url(url: &str, error: &str) -> Option<String> {
|
||||
if mysql_connection_charset(url).is_some_and(|charset| !charset.eq_ignore_ascii_case(MYSQL_DEFAULT_CHARSET)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lower = error.to_ascii_lowercase();
|
||||
// 1115 is ER_UNKNOWN_CHARACTER_SET; MySQL-compatible servers may report the
|
||||
// same rejection with a different code but the same message.
|
||||
if !lower.contains("unknown character set") && !lower.contains("1115") {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(mysql_url_with_charset(url, MYSQL_LEGACY_CHARSET))
|
||||
}
|
||||
|
||||
fn mysql_url_with_charset(url: &str, charset: &str) -> String {
|
||||
let (base_url, fragment) = url.split_once('#').map_or((url, ""), |(base, fragment)| (base, fragment));
|
||||
let charset_param = format!("charset={charset}");
|
||||
let base = match base_url.split_once('?') {
|
||||
Some((prefix, query)) => {
|
||||
let mut params: Vec<String> = query
|
||||
.split('&')
|
||||
.filter(|part| !part.trim().is_empty())
|
||||
.filter(|part| !part.split_once('=').is_some_and(|(key, _)| key.trim().eq_ignore_ascii_case("charset")))
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
params.push(charset_param);
|
||||
format!("{prefix}?{}", params.join("&"))
|
||||
}
|
||||
None => format!("{base_url}?{charset_param}"),
|
||||
};
|
||||
|
||||
if fragment.is_empty() {
|
||||
base
|
||||
} else {
|
||||
format!("{base}#{fragment}")
|
||||
}
|
||||
}
|
||||
|
||||
fn mysql_group_concat_setup_fallback_mode(setup_mode: MySqlSetupMode, error: &str) -> Option<MySqlSetupMode> {
|
||||
@@ -1618,7 +1696,7 @@ fn mysql_setup_queries_for_database_with_mode(
|
||||
extra_setup_queries: &[String],
|
||||
setup_mode: MySqlSetupMode,
|
||||
) -> Vec<String> {
|
||||
let charset = mysql_connection_charset(url).unwrap_or("utf8mb4");
|
||||
let charset = mysql_connection_charset(url).unwrap_or(MYSQL_DEFAULT_CHARSET);
|
||||
let database = setup_database.map(ToOwned::to_owned).or_else(|| mysql_connection_database(url));
|
||||
let mut queries = Vec::new();
|
||||
if let Some(database) = database.as_deref() {
|
||||
@@ -8058,6 +8136,89 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_legacy_charset_fallback_retries_with_utf8_for_old_servers() {
|
||||
let error = "MySQL connection failed: Server error: `ERROR 1115 (42000): Unknown character set: 'utf8mb4''";
|
||||
|
||||
assert_eq!(
|
||||
mysql_legacy_charset_fallback_url("mysql://root:pw@host:3306/app", error),
|
||||
Some("mysql://root:pw@host:3306/app?charset=utf8".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_legacy_charset_fallback_url("mysql://root:pw@host:3306/app?ssl-mode=disabled#session", error),
|
||||
Some("mysql://root:pw@host:3306/app?ssl-mode=disabled&charset=utf8#session".to_string())
|
||||
);
|
||||
// MySQL-compatible servers may report the same rejection with another code.
|
||||
assert!(mysql_legacy_charset_fallback_url(
|
||||
"mysql://root@host:9030/app",
|
||||
"Server error: Unknown character set: 'utf8mb4'"
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_legacy_charset_fallback_retries_the_built_in_default_charset() {
|
||||
let error = "MySQL connection failed: Server error: `ERROR 1115 (42000): Unknown character set: 'utf8mb4''";
|
||||
|
||||
// The config layer writes the built-in default into every MySQL URL, so the
|
||||
// default spelling must stay retryable.
|
||||
assert_eq!(
|
||||
mysql_legacy_charset_fallback_url(
|
||||
"mysql://root:secret@127.0.0.1:3306/app?ssl-mode=disabled&charset=utf8mb4",
|
||||
error
|
||||
),
|
||||
Some("mysql://root:secret@127.0.0.1:3306/app?ssl-mode=disabled&charset=utf8".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_legacy_charset_fallback_url("mysql://root@host/app?charset=UTF8MB4", error),
|
||||
Some("mysql://root@host/app?charset=utf8".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_legacy_charset_fallback_keeps_another_configured_charset() {
|
||||
let error = "MySQL connection failed: Server error: `ERROR 1115 (42000): Unknown character set: 'utf8mb4''";
|
||||
|
||||
// A charset other than the built-in default is the user's own choice; the
|
||||
// retry must not override it.
|
||||
assert_eq!(mysql_legacy_charset_fallback_url("mysql://root@host/app?charset=latin1", error), None);
|
||||
assert_eq!(mysql_legacy_charset_fallback_url("mysql://root@host/app?charset=gbk", error), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_legacy_charset_fallback_ignores_unrelated_connection_errors() {
|
||||
// A credential failure has nothing to do with the built-in charset setup.
|
||||
assert_eq!(
|
||||
mysql_legacy_charset_fallback_url(
|
||||
"mysql://root@host/app",
|
||||
"MySQL connection failed: Server error: `ERROR 1045 (28000): Access denied for user 'root'@'host'`"
|
||||
),
|
||||
None
|
||||
);
|
||||
|
||||
// A server-side rejection of another setup statement must not rewrite the charset.
|
||||
assert_eq!(
|
||||
mysql_legacy_charset_fallback_url(
|
||||
"mysql://root@host/app",
|
||||
"MySQL connection failed: Server error: `ERROR HY000 (1193): Unknown system variable,stmt:SET @@group_concat_max_len = 1048576'"
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_url_with_charset_replaces_an_existing_charset_param() {
|
||||
assert_eq!(mysql_url_with_charset("mysql://root@host/app", "utf8"), "mysql://root@host/app?charset=utf8");
|
||||
assert_eq!(
|
||||
mysql_url_with_charset("mysql://root@host/app?charset=utf8mb4&ssl-mode=disabled", "utf8"),
|
||||
"mysql://root@host/app?ssl-mode=disabled&charset=utf8"
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_url_with_charset("mysql://root@host/app?ssl-mode=disabled#session", "utf8"),
|
||||
"mysql://root@host/app?ssl-mode=disabled&charset=utf8#session"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_index_metadata_query_has_expression_compatibility_fallback() {
|
||||
let with_expression = mysql_list_indexes_sql("db", "users", true);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Reference in new issue
Block a user