fix(export): declare utf8mb4 charset in MySQL export script

This commit is contained in:
zipg authored and GitHub committed 2026-09-25 10:34:55 +08:00
1 parent 06b09768c2
commit 2b8329f0f2
2 files changed
+26

No files matched your search

@@ -30,6 +30,15 @@ static EXPORT_CANCELLED: std::sync::LazyLock<RwLock<HashSet<String>>> =
const EXPORT_CANCELLED_ERROR: &str = "Export cancelled";
const EXPORT_CANCEL_POLL_INTERVAL: Duration = Duration::from_millis(50);
/// MySQL scripts written by the exporter are always UTF-8: values are decoded off the
/// wire and reformatted into Rust strings before they are written, so the file encoding
/// never follows the connection charset. Declaring that encoding in the script keeps the
/// importing client's default out of the decoding decision -- a `latin1` `mysql` CLI (a
/// docker entrypoint initializing a container, for example) would otherwise re-encode
/// every non-ASCII value into mojibake (#10242). `mysqldump` writes the same statement
/// for the same reason.
const MYSQL_EXPORT_CHARSET_STATEMENT: &str = "SET NAMES utf8mb4;";
pub fn database_export_client_session_id(export_id: &str) -> String {
task_client_session_id("database-export", export_id)
}
@@ -3020,6 +3029,13 @@ async fn export_database_sql_core_inner(
writeln!(file, "-- Generated by DBX").map_err(|e| format!("Failed to write file: {e}"))?;
writeln!(file).map_err(|e| format!("Failed to write file: {e}"))?;
// Declare the script's encoding before the first statement that can carry non-ASCII
// text (the database name is written by the preamble right below, and table, column
// and DEFAULT values follow).
if matches!(db_type, DatabaseType::Mysql) {
writeln!(file, "{MYSQL_EXPORT_CHARSET_STATEMENT}").map_err(|e| format!("Failed to write file: {e}"))?;
}
if let Some(preamble) = create_database_preamble {
writeln!(file, "-- Database setup").map_err(|e| format!("Failed to write file: {e}"))?;
writeln!(file, "{preamble}").map_err(|e| format!("Failed to write file: {e}"))?;
@@ -404,6 +404,16 @@ async fn live_mysql_database_export_creates_missing_destination_directory() {
let exported = std::fs::read_to_string(&file_path).unwrap();
assert!(exported.contains("'alpha'"), "exported SQL should contain the seeded row");
// #10242: the script must declare its own encoding before the first statement that can
// carry non-ASCII text, otherwise an importing client whose default charset is not
// utf8mb4 (a `latin1` mysql CLI in a docker entrypoint, for example) re-encodes every
// non-ASCII value the exporter wrote as UTF-8 into mojibake.
let charset = exported.find("SET NAMES utf8mb4;").expect("MySQL exports must declare their encoding");
let create_database = exported.find("CREATE DATABASE").expect("the CREATE DATABASE preamble should be exported");
let create_table = exported.find("CREATE TABLE").expect("the table DDL should be exported");
assert!(charset < create_database, "SET NAMES must precede the CREATE DATABASE preamble");
assert!(charset < create_table, "SET NAMES must precede the table DDL");
std::fs::remove_dir_all(dir).unwrap();
}