diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index dab6f5a80..5d58938ed 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -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::::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::::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::::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::::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::::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'"))); +} diff --git a/crates/dbx-core/src/table_structure_sql/util.rs b/crates/dbx-core/src/table_structure_sql/util.rs index 9e793c294..1167fb7d1 100644 --- a/crates/dbx-core/src/table_structure_sql/util.rs +++ b/crates/dbx-core/src/table_structure_sql/util.rs @@ -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::>().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 {