fix(export): preserve MySQL typed literals

This commit is contained in:
t8y2 2026-07-06 13:48:36 +08:00
parent 0c965eb836
commit dd8d1d3556
1 changed files with 104 additions and 0 deletions

View File

@ -182,6 +182,18 @@ fn format_export_sql_literal_typed(
if matches!(database_type, Some(DatabaseType::Mysql)) && column_type.is_some_and(is_mysql_bit_type) {
return format_mysql_bit_literal(value);
}
if is_mysql_compatible_export_literal_target(database_type) {
if column_type.is_some_and(is_mysql_binary_export_type) {
if let Some(literal) = format_mysql_binary_export_literal(value) {
return literal;
}
}
if column_type.is_some_and(is_export_numeric_type) {
if let Some(literal) = format_export_numeric_literal(value) {
return literal;
}
}
}
if let Some(arr) = value.as_array() {
if matches!(database_type, Some(DatabaseType::ClickHouse) | Some(DatabaseType::Databend)) {
return format_ch_array_sql_literal(arr);
@ -407,6 +419,72 @@ fn format_mysql_bit_literal(value: &Value) -> String {
}
}
fn is_mysql_binary_export_type(column_type: &str) -> bool {
let lower = column_type.trim().to_ascii_lowercase();
let base = lower.split(['(', ':', ' ', '\t', '\n']).next().unwrap_or("").trim();
matches!(base, "binary" | "varbinary" | "blob" | "tinyblob" | "mediumblob" | "longblob")
}
fn format_mysql_binary_export_literal(value: &Value) -> Option<String> {
match value {
Value::Null => Some("NULL".to_string()),
Value::String(text) => format_mysql_binary_export_literal_text(text),
_ => None,
}
}
fn format_mysql_binary_export_literal_text(text: &str) -> Option<String> {
let trimmed = text.trim();
let hex = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X"))?;
if hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
// DBX exposes MySQL binary cells as 0x-prefixed hex text. Keep it as a
// MySQL hex literal so exported INSERT statements round-trip bytes.
Some(if hex.is_empty() { "X''".to_string() } else { format!("0x{hex}") })
} else {
None
}
}
fn is_export_numeric_type(column_type: &str) -> bool {
let lower = column_type.to_ascii_lowercase();
[
"int",
"integer",
"bigint",
"smallint",
"tinyint",
"mediumint",
"serial",
"number",
"numeric",
"decimal",
"dec",
"fixed",
"float",
"double",
"real",
]
.iter()
.any(|part| lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == *part))
}
fn format_export_numeric_literal(value: &Value) -> Option<String> {
match value {
Value::Number(number) => Some(number.to_string()),
Value::String(text) if is_export_numeric_literal(text) => Some(text.to_string()),
_ => None,
}
}
fn is_export_numeric_literal(text: &str) -> bool {
if text.trim() != text || text.is_empty() {
return false;
}
text.parse::<f64>().is_ok_and(f64::is_finite)
&& text.chars().all(|ch| ch.is_ascii_digit() || matches!(ch, '+' | '-' | '.' | 'e' | 'E'))
&& text.chars().any(|ch| ch.is_ascii_digit())
}
pub fn build_export_insert_statements(options: BuildExportInsertStatementsOptions) -> Result<Vec<String>, String> {
if options.columns.is_empty() || options.rows.is_empty() {
return Ok(Vec::new());
@ -1441,6 +1519,32 @@ mod tests {
);
}
#[test]
fn mysql_export_uses_typed_literals_for_numeric_and_blob_columns() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: Some("t_test_01".to_string()),
qualified_table_name: None,
columns: vec!["id".to_string(), "f_blob".to_string(), "note".to_string()],
column_types: vec![Some("int".to_string()), Some("blob".to_string()), Some("varchar(64)".to_string())],
column_extras: Vec::new(),
rows: vec![
vec![json!("1"), json!("0x68656c6c6f"), json!("0x68656c6c6f")],
vec![json!("2"), json!("0X"), json!("1")],
],
batch_size: Some(10),
})
.unwrap();
assert_eq!(
statements,
vec![
"INSERT INTO `t_test_01` (`id`, `f_blob`, `note`) VALUES (1, 0x68656c6c6f, '0x68656c6c6f'), (2, X'', '1');"
]
);
}
#[test]
fn temporal_columns_export_without_rfc3339_separator_or_utc_suffix() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {