fix(structure): emit NULL keyword for nullable MySQL timestamp columns
This commit is contained in:
1 parent
1016ccc352
commit
9290391985
3 files changed
+147
-3
No files matched your search
@@ -26,6 +26,12 @@ pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStru
|
||||
}
|
||||
if !column.is_nullable && !is_oracle_like(dialect) && dialect != StructureDialect::ClickHouse {
|
||||
parts.push("NOT NULL".to_string());
|
||||
} else if column.is_nullable
|
||||
&& dialect == StructureDialect::Mysql
|
||||
&& mysql_generated_clause.is_none()
|
||||
&& is_mysql_timestamp_type(&column.data_type)
|
||||
{
|
||||
parts.push("NULL".to_string());
|
||||
}
|
||||
if mysql_generated_clause.is_none() {
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
@@ -438,3 +444,18 @@ pub(super) fn is_mysql_character_data_type(data_type: &str) -> bool {
|
||||
let normalized = base_type.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
|
||||
matches!(normalized.as_str(), "char" | "varchar" | "tinytext" | "text" | "mediumtext" | "longtext" | "enum" | "set")
|
||||
}
|
||||
|
||||
/// MySQL silently rewrites a nullable `TIMESTAMP` column to `NOT NULL` when the
|
||||
/// generated DDL omits an explicit `NULL` keyword — regardless of whether a
|
||||
/// `DEFAULT` is present. With the (still common) `explicit_defaults_for_timestamp`
|
||||
/// server default of `OFF`, this can outright fail with `ERROR 1067 (42000):
|
||||
/// Invalid default value` for any non-first TIMESTAMP column. `DATETIME` is not
|
||||
/// affected and must not be touched.
|
||||
pub(super) fn is_mysql_timestamp_type(data_type: &str) -> bool {
|
||||
let trimmed = data_type.trim();
|
||||
let base_type = match trimmed.find('(') {
|
||||
Some(open_index) => trimmed[..open_index].trim(),
|
||||
None => trimmed,
|
||||
};
|
||||
base_type.eq_ignore_ascii_case("timestamp")
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::column_format::{
|
||||
column_data_type, column_extra_clause, has_dameng_identity, is_dameng_identity_compatible_type,
|
||||
is_mysql_character_data_type,
|
||||
is_mysql_character_data_type, is_mysql_timestamp_type,
|
||||
};
|
||||
use super::comments::{build_sqlserver_column_comment_sql, build_sqlserver_table_comment_sql};
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
@@ -75,6 +75,12 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
|
||||
&& !matches!(dialect, StructureDialect::ClickHouse | StructureDialect::ManticoreSearch)
|
||||
{
|
||||
parts.push("NOT NULL".to_string());
|
||||
} else if column.is_nullable
|
||||
&& !column.is_primary_key
|
||||
&& dialect == StructureDialect::Mysql
|
||||
&& is_mysql_timestamp_type(&column.data_type)
|
||||
{
|
||||
parts.push("NULL".to_string());
|
||||
}
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
parts.push(extra_clause);
|
||||
|
||||
@@ -1746,7 +1746,7 @@ fn mysql_add_timestamp_column_drops_invalid_precision() {
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["ALTER TABLE `users` ADD COLUMN `created_at` timestamp DEFAULT CURRENT_TIMESTAMP;"]
|
||||
vec!["ALTER TABLE `users` ADD COLUMN `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP;"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1774,7 +1774,7 @@ fn mysql_add_timestamp_column_preserves_valid_precision() {
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["ALTER TABLE `users` ADD COLUMN `created_at` timestamp(3) DEFAULT CURRENT_TIMESTAMP(3);"]
|
||||
vec!["ALTER TABLE `users` ADD COLUMN `created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3);"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6551,3 +6551,120 @@ fn gaussdb_m_create_table_with_primary_key() {
|
||||
let sql = result.statements.join("\n");
|
||||
assert!(sql.contains("PRIMARY KEY (`id`)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_table_nullable_timestamp_without_default_gets_explicit_null() {
|
||||
// A second (or later) MySQL TIMESTAMP column that is nullable but has no
|
||||
// DEFAULT must carry an explicit NULL keyword — otherwise MySQL either
|
||||
// silently rewrites it to NOT NULL or, with the still-common
|
||||
// explicit_defaults_for_timestamp=OFF server default, rejects it outright
|
||||
// with ERROR 1067 (42000): Invalid default value. See issue #7416.
|
||||
let mut created_at = column("created_at");
|
||||
created_at.data_type = "timestamp".to_string();
|
||||
created_at.is_nullable = false;
|
||||
|
||||
let mut updated_at = column("updated_at");
|
||||
updated_at.data_type = "timestamp".to_string();
|
||||
updated_at.is_nullable = true;
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "u7_game_order_step".to_string(),
|
||||
columns: vec![created_at, updated_at],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
mysql_engine: None,
|
||||
partitioned: false,
|
||||
is_gaussdb_m_mode: false,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert!(result.statements[0].contains("`updated_at` timestamp NULL"));
|
||||
assert!(!result.statements[0].contains("`updated_at` timestamp NULL DEFAULT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_table_nullable_timestamp_with_default_still_gets_explicit_null() {
|
||||
// Even with an explicit DEFAULT, MySQL still needs the NULL keyword to
|
||||
// keep the column nullable — omitting it silently produces NOT NULL.
|
||||
let mut updated_at = column("updated_at");
|
||||
updated_at.data_type = "timestamp".to_string();
|
||||
updated_at.is_nullable = true;
|
||||
updated_at.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![updated_at],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
mysql_engine: None,
|
||||
partitioned: false,
|
||||
is_gaussdb_m_mode: false,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert!(result.statements[0].contains("`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_table_nullable_datetime_does_not_gain_null_keyword() {
|
||||
// DATETIME is not subject to MySQL's TIMESTAMP-specific implicit-default
|
||||
// quirk; the fix must not touch it.
|
||||
let mut updated_at = column("updated_at");
|
||||
updated_at.data_type = "datetime".to_string();
|
||||
updated_at.is_nullable = true;
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![updated_at],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
mysql_engine: None,
|
||||
partitioned: false,
|
||||
is_gaussdb_m_mode: false,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert!(!result.statements[0].contains("NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_add_column_nullable_timestamp_without_default_gets_explicit_null() {
|
||||
// The ADD COLUMN / MODIFY COLUMN / CHANGE COLUMN path shares
|
||||
// column_definition() with CREATE TABLE and must carry the same fix.
|
||||
let mut updated_at = column("updated_at");
|
||||
updated_at.data_type = "timestamp".to_string();
|
||||
updated_at.is_nullable = true;
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "u7_game_order_step".to_string(),
|
||||
columns: vec![updated_at],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
mysql_engine: None,
|
||||
partitioned: false,
|
||||
is_gaussdb_m_mode: false,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["ALTER TABLE `u7_game_order_step` ADD COLUMN `updated_at` timestamp NULL;"]);
|
||||
}
|
||||
Reference in new issue
Block a user