fix(table-structure): quote default values for string column types

This commit is contained in:
t8y2 2026-06-17 22:36:05 +08:00
parent 268ee99328
commit 48af146b95
2 changed files with 237 additions and 7 deletions

View File

@ -65,7 +65,7 @@ fn builds_mysql_column_and_index_changes() {
let mut renamed = column("display_name");
renamed.data_type = "varchar(120)".to_string();
renamed.is_nullable = false;
renamed.default_value = "'guest'".to_string();
renamed.default_value = "guest".to_string();
renamed.comment = "Shown name".to_string();
renamed.original = Some(ColumnInfo {
name: "name".to_string(),
@ -122,7 +122,7 @@ fn builds_informix_column_and_index_changes() {
let mut renamed = column("display_name");
renamed.data_type = "varchar(120)".to_string();
renamed.is_nullable = false;
renamed.default_value = "'guest'".to_string();
renamed.default_value = "guest".to_string();
renamed.original = Some(ColumnInfo {
name: "name".to_string(),
data_type: "varchar(80)".to_string(),
@ -887,7 +887,7 @@ fn builds_h2_schema_qualified_existing_column_statements() {
name.id = "name".to_string();
name.data_type = "VARCHAR(120)".to_string();
name.is_nullable = false;
name.default_value = "'guest'".to_string();
name.default_value = "guest".to_string();
name.comment = "Display name".to_string();
name.original = Some(ColumnInfo {
name: "NAME".to_string(),
@ -1496,3 +1496,140 @@ fn builds_mysql_trigger_changes() {
]
);
}
#[test]
fn mysql_varchar_default_is_quoted() {
let mut col = column("name");
col.data_type = "varchar(255)".to_string();
col.default_value = "hello".to_string();
let result = build_create_table_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: "users".to_string(),
columns: vec![col],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert!(result.statements[0].contains("DEFAULT 'hello'"));
assert!(!result.statements[0].contains("DEFAULT hello "));
}
#[test]
fn mysql_char_default_is_quoted() {
let mut col = column("code");
col.data_type = "char(10)".to_string();
col.default_value = "abc".to_string();
let result = build_create_table_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: "items".to_string(),
columns: vec![col],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert!(result.statements[0].contains("DEFAULT 'abc'"));
}
#[test]
fn mysql_text_default_is_quoted() {
let mut col = column("description");
col.data_type = "text".to_string();
col.default_value = "default value".to_string();
let result = build_create_table_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: "products".to_string(),
columns: vec![col],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert!(result.statements[0].contains("DEFAULT 'default value'"));
}
#[test]
fn mysql_enum_default_is_quoted() {
let mut col = column("status");
col.data_type = "enum('active','inactive')".to_string();
col.default_value = "active".to_string();
let result = build_create_table_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: "users".to_string(),
columns: vec![col],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert!(result.statements[0].contains("DEFAULT 'active'"));
}
#[test]
fn mysql_int_default_is_not_quoted() {
let mut col = column("score");
col.data_type = "int".to_string();
col.default_value = "100".to_string();
let result = build_create_table_sql(TableStructureSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
table_name: "games".to_string(),
columns: vec![col],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
});
assert_eq!(result.warnings, Vec::<String>::new());
assert!(result.statements[0].contains("DEFAULT 100"));
assert!(!result.statements[0].contains("DEFAULT '100'"));
}
#[test]
fn postgres_varchar_default_is_quoted() {
let mut col = column("label");
col.data_type = "varchar(100)".to_string();
col.default_value = "test label".to_string();
col.original = Some(ColumnInfo {
name: "label".to_string(),
data_type: "varchar(100)".to_string(),
is_nullable: true,
column_default: None,
is_primary_key: false,
extra: None,
comment: Some(String::new()),
});
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
database_type: Some(DatabaseType::Postgres),
schema: None,
table_name: "items".to_string(),
column: col,
});
assert!(result.statements.iter().any(|s| s.contains("SET DEFAULT 'test label'")));
}

View File

@ -114,16 +114,109 @@ pub(super) fn is_temporal_expression(value: &str) -> bool {
trimmed.chars().all(|c| c.is_ascii_alphabetic() || c == '_')
}
pub(super) fn is_string_type_for_default(dialect: StructureDialect, base_type: &str) -> bool {
let normalized = base_type.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
match dialect {
StructureDialect::Mysql => matches!(
normalized.as_str(),
"char"
| "varchar"
| "tinytext"
| "text"
| "mediumtext"
| "longtext"
| "binary"
| "varbinary"
| "tinyblob"
| "blob"
| "mediumblob"
| "longblob"
| "enum"
| "set"
| "json"
| "nvarchar"
| "nchar"
| "long"
),
StructureDialect::Postgres => matches!(
normalized.as_str(),
"char"
| "character"
| "varchar"
| "character varying"
| "text"
| "bpchar"
| "name"
| "json"
| "jsonb"
| "xml"
| "bytea"
| "uuid"
),
StructureDialect::SqlServer => matches!(
normalized.as_str(),
"char" | "varchar" | "nchar" | "nvarchar" | "text" | "ntext" | "xml" | "uniqueidentifier" | "sysname"
),
StructureDialect::Oracle => matches!(
normalized.as_str(),
"char" | "nchar" | "varchar2" | "nvarchar2" | "clob" | "nclob" | "long" | "raw" | "long raw" | "bfile"
),
StructureDialect::H2 => matches!(
normalized.as_str(),
"char"
| "character"
| "varchar"
| "character varying"
| "text"
| "clob"
| "binary"
| "varbinary"
| "blob"
| "uuid"
| "json"
),
StructureDialect::ClickHouse => matches!(normalized.as_str(), "string" | "fixedstring" | "uuid" | "json"),
StructureDialect::Sqlite => {
matches!(normalized.as_str(), "text" | "varchar" | "char" | "character" | "clob" | "nvarchar" | "nchar")
}
StructureDialect::Informix => matches!(
normalized.as_str(),
"char"
| "character"
| "varchar"
| "character varying"
| "nvarchar"
| "nchar"
| "text"
| "clob"
| "lvarchar"
| "byte"
| "blob"
),
_ => false,
}
}
pub(super) fn format_default_for_sql(dialect: StructureDialect, data_type: &str, default_value: &str) -> String {
if default_value.is_empty() {
return String::new();
}
let base_type = data_type.split('(').next().unwrap_or(data_type).trim();
if is_temporal_type_for_default(dialect, base_type) && !is_temporal_expression(default_value) {
quote_string(default_value)
} else {
default_value.to_string()
if is_temporal_type_for_default(dialect, base_type) {
if is_temporal_expression(default_value) {
return default_value.to_string();
}
return quote_string(default_value);
}
if is_string_type_for_default(dialect, base_type) {
// Only skip quoting for function-call expressions like `gen_random_uuid()`.
// Simple identifiers like `CURRENT_TIMESTAMP` are not valid defaults for string columns.
if default_value.contains('(') || default_value.contains(')') {
return default_value.to_string();
}
return quote_string(default_value);
}
default_value.to_string()
}
pub(super) fn normalize_default(value: Option<&String>) -> String {