fix(mysql): preserve string defaults in schema diff

This commit is contained in:
t8y2 2026-07-11 19:38:28 +08:00
parent 1dd4623919
commit c2040021f6
1 changed files with 132 additions and 0 deletions

View File

@ -984,12 +984,110 @@ fn quote_id(name: &str, db_type: DatabaseType) -> String {
}
}
fn mysql_default_value(default: &str, data_type: &str) -> String {
let trimmed = default.trim();
if is_mysql_default_expression(trimmed) || !is_mysql_string_literal_type(data_type) {
return default.to_string();
}
// MySQL metadata returns string defaults as values rather than reusable DDL tokens.
let literal = format!("'{}'", default.replace('\'', "''"));
if mysql_default_requires_expression(data_type) {
format!("({literal})")
} else {
literal
}
}
fn is_mysql_default_expression(default: &str) -> bool {
let upper = default.to_ascii_uppercase();
(default.starts_with('(') && default.ends_with(')'))
|| (default.starts_with('\'') && default.ends_with('\''))
|| (default.starts_with('"') && default.ends_with('"'))
|| upper == "NULL"
|| upper == "CURRENT_TIMESTAMP"
|| upper.strip_prefix("CURRENT_TIMESTAMP(").is_some_and(|precision| {
precision.ends_with(')') && precision[..precision.len() - 1].chars().all(|c| c.is_ascii_digit())
})
|| matches!(upper.as_bytes(), [b'B' | b'X', b'\'', .., b'\''])
}
fn is_mysql_string_literal_type(data_type: &str) -> bool {
let base_type = mysql_base_type(data_type);
matches!(
base_type.as_str(),
"char"
| "varchar"
| "tinytext"
| "text"
| "mediumtext"
| "longtext"
| "tinyblob"
| "blob"
| "mediumblob"
| "longblob"
| "binary"
| "varbinary"
| "enum"
| "set"
| "date"
| "datetime"
| "timestamp"
| "time"
| "year"
| "json"
| "geometry"
| "point"
| "linestring"
| "polygon"
| "multipoint"
| "multilinestring"
| "multipolygon"
| "geometrycollection"
)
}
fn mysql_default_requires_expression(data_type: &str) -> bool {
let base_type = mysql_base_type(data_type);
matches!(
base_type.as_str(),
"tinytext"
| "text"
| "mediumtext"
| "longtext"
| "tinyblob"
| "blob"
| "mediumblob"
| "longblob"
| "json"
| "geometry"
| "point"
| "linestring"
| "polygon"
| "multipoint"
| "multilinestring"
| "multipolygon"
| "geometrycollection"
)
}
fn mysql_base_type(data_type: &str) -> String {
data_type
.trim_start()
.split(|c: char| c == '(' || c.is_ascii_whitespace())
.next()
.unwrap_or_default()
.to_ascii_lowercase()
}
fn column_def(col: &ColumnInfo, db_type: DatabaseType) -> String {
let mut definition = format!("{} {}", quote_id(&col.name, db_type), col.data_type);
if !col.is_nullable {
definition.push_str(" NOT NULL");
}
if let Some(default) = &col.column_default {
let default =
if db_type == DatabaseType::Mysql { mysql_default_value(default, &col.data_type) } else { default.clone() };
definition.push_str(&format!(" DEFAULT {default}"));
}
if is_mysql_like(db_type) {
@ -1513,6 +1611,40 @@ mod tests {
}
}
#[test]
fn formats_mysql_metadata_defaults_as_valid_column_definitions() {
let cases = [
("varchar(50)", "外籍一般件", "DEFAULT '外籍一般件'"),
("VARCHAR(50)", "uppercase type", "DEFAULT 'uppercase type'"),
("varchar(50)", "", "DEFAULT ''"),
("varchar(50)", "O'Reilly\\docs", "DEFAULT 'O''Reilly\\docs'"),
("int", "7", "DEFAULT 7"),
("decimal(10,2)", "1.00", "DEFAULT 1.00"),
("timestamp", "CURRENT_TIMESTAMP", "DEFAULT CURRENT_TIMESTAMP"),
("timestamp(3)", "CURRENT_TIMESTAMP(3)", "DEFAULT CURRENT_TIMESTAMP(3)"),
("varchar(36)", "(UUID())", "DEFAULT (UUID())"),
("json", "(JSON_ARRAY())", "DEFAULT (JSON_ARRAY())"),
("text", "plain text", "DEFAULT ('plain text')"),
("json", "{}", "DEFAULT ('{}')"),
("varchar(50)", "'already quoted'", "DEFAULT 'already quoted'"),
];
for (data_type, default, expected) in cases {
let mut source = column("value", data_type, None);
source.column_default = Some(default.to_string());
assert!(column_def(&source, DatabaseType::Mysql).contains(expected), "{data_type}: {default}");
}
}
#[test]
fn leaves_non_mysql_defaults_unchanged() {
let mut source = column("value", "varchar(50)", None);
source.column_default = Some("unquoted_metadata_value".to_string());
assert!(column_def(&source, DatabaseType::Postgres).contains("DEFAULT unquoted_metadata_value"));
assert!(column_def(&source, DatabaseType::Doris).contains("DEFAULT unquoted_metadata_value"));
}
#[test]
fn ignores_column_order_when_option_is_disabled() {
let diffs = diff_columns_with_options(