fix(mysql): carry datetime precision into current_timestamp clauses
Closes #8402
This commit is contained in:
4 files changed
+192
-4
No files matched your search
@@ -79,7 +79,7 @@ pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStru
|
||||
if mysql_generated_clause.is_none() {
|
||||
if let Some(on_update) = column.extra.as_ref().and_then(|e| e.on_update_current_timestamp).filter(|v| *v) {
|
||||
if on_update && dialect == StructureDialect::Mysql {
|
||||
parts.push("ON UPDATE CURRENT_TIMESTAMP".to_string());
|
||||
parts.push(mysql_on_update_current_timestamp_clause(&column.data_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,6 +420,39 @@ pub(super) fn is_valid_temporal_precision(params: &str, dialect: StructureDialec
|
||||
value <= max && params == value.to_string()
|
||||
}
|
||||
|
||||
/// Fractional-seconds precision of a MySQL temporal column type, e.g. `3` for
|
||||
/// `datetime(3)`. MySQL requires every `CURRENT_TIMESTAMP` in a column
|
||||
/// definition to carry the same precision the column type declares: a
|
||||
/// `datetime(3) ... DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`
|
||||
/// definition is rejected with `ERROR 1067 (42000): Invalid default value`
|
||||
/// while the `(3)`-suffixed clauses succeed. The editor model keeps only an
|
||||
/// on-update flag, so the precision has to be recovered from the data type.
|
||||
/// Non-temporal types and invalid precisions fall back to `None` (no suffix).
|
||||
pub(super) fn mysql_temporal_precision(data_type: &str) -> Option<String> {
|
||||
let trimmed = data_type.trim();
|
||||
let open_index = trimmed.find('(')?;
|
||||
if !trimmed.ends_with(')') {
|
||||
return None;
|
||||
}
|
||||
let base_type = trimmed[..open_index].trim();
|
||||
let params = trimmed[open_index + 1..trimmed.len() - 1].trim();
|
||||
if !is_temporal_precision_type(StructureDialect::Mysql, base_type)
|
||||
|| !is_valid_temporal_precision(params, StructureDialect::Mysql)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(params.to_string())
|
||||
}
|
||||
|
||||
/// `ON UPDATE CURRENT_TIMESTAMP` clause for a MySQL column, carrying the
|
||||
/// column's temporal precision (if any) so the clause matches the type.
|
||||
pub(super) fn mysql_on_update_current_timestamp_clause(data_type: &str) -> String {
|
||||
match mysql_temporal_precision(data_type) {
|
||||
Some(fsp) => format!("ON UPDATE CURRENT_TIMESTAMP({fsp})"),
|
||||
None => "ON UPDATE CURRENT_TIMESTAMP".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clickhouse_column_type(column: &EditableStructureColumn) -> String {
|
||||
let data_type = column.data_type.trim();
|
||||
if column.is_nullable {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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_timestamp_type, strip_inherited_mysql_column_charsets,
|
||||
is_mysql_character_data_type, is_mysql_timestamp_type, mysql_on_update_current_timestamp_clause,
|
||||
strip_inherited_mysql_column_charsets,
|
||||
};
|
||||
use super::comments::{build_sqlserver_column_comment_sql, build_sqlserver_table_comment_sql};
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
@@ -94,7 +95,7 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
|
||||
}
|
||||
if let Some(on_update) = column.extra.as_ref().and_then(|e| e.on_update_current_timestamp).filter(|v| *v) {
|
||||
if on_update && dialect == StructureDialect::Mysql {
|
||||
parts.push("ON UPDATE CURRENT_TIMESTAMP".to_string());
|
||||
parts.push(mysql_on_update_current_timestamp_clause(&column.data_type));
|
||||
}
|
||||
}
|
||||
if dialect == StructureDialect::Mysql && capabilities.comment && !clean(&column.comment).is_empty() {
|
||||
|
||||
@@ -5060,6 +5060,138 @@ fn mysql_create_table_with_on_update_current_timestamp() {
|
||||
assert!(result.statements[0].contains("ON UPDATE CURRENT_TIMESTAMP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_table_carries_temporal_precision_into_current_timestamp_clauses() {
|
||||
let mut col = column("updated_at");
|
||||
col.data_type = "datetime(3)".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(structure_change_options(DatabaseType::Mysql, None, "users", vec![col]));
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["CREATE TABLE `users` (\n `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)\n);"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_table_keeps_explicit_zero_temporal_precision_in_current_timestamp_clauses() {
|
||||
let mut col = column("updated_at");
|
||||
col.data_type = "datetime(0)".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(structure_change_options(DatabaseType::Mysql, None, "users", vec![col]));
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["CREATE TABLE `users` (\n `updated_at` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0)\n);"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_single_column_alter_carries_temporal_precision_into_current_timestamp_clauses() {
|
||||
let mut col = column("updated_at");
|
||||
col.data_type = "datetime(3)".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
col.original = Some(ColumnInfo {
|
||||
name: "updated_at".to_string(),
|
||||
data_type: "datetime".to_string(),
|
||||
is_nullable: false,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
driver_profile: None,
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
column: col,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["ALTER TABLE `users` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3);"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_single_column_alter_resaving_precision_column_keeps_temporal_precision() {
|
||||
let mut col = column("updated_at");
|
||||
col.data_type = "datetime(3)".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP(3)".to_string();
|
||||
col.comment = "updated".to_string();
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
col.original = Some(ColumnInfo {
|
||||
name: "updated_at".to_string(),
|
||||
data_type: "datetime(3)".to_string(),
|
||||
is_nullable: false,
|
||||
column_default: Some("CURRENT_TIMESTAMP(3)".to_string()),
|
||||
is_primary_key: false,
|
||||
extra: Some("on update CURRENT_TIMESTAMP(3)".to_string()),
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
driver_profile: None,
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
column: col,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["ALTER TABLE `users` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'updated';"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_current_timestamp_clauses_stay_bare_without_column_precision() {
|
||||
let mut col = column("updated_at");
|
||||
col.data_type = "timestamp".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(structure_change_options(DatabaseType::Mysql, None, "users", vec![col]));
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["CREATE TABLE `users` (\n `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n);"]
|
||||
);
|
||||
|
||||
let mut col = column("updated_at");
|
||||
col.data_type = "datetime(7)".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(structure_change_options(DatabaseType::Mysql, None, "users", vec![col]));
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["CREATE TABLE `users` (\n `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n);"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_create_table_with_identity() {
|
||||
let mut col = column("id");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::column_format::mysql_temporal_precision;
|
||||
use super::dialect::StructureDialect;
|
||||
use super::types::EditableStructureColumn;
|
||||
use crate::models::connection::DatabaseType;
|
||||
@@ -372,7 +373,7 @@ pub(super) fn format_default_for_sql(dialect: StructureDialect, data_type: &str,
|
||||
let base_type = data_type.split('(').next().unwrap_or(data_type).trim();
|
||||
if is_temporal_type_for_default(dialect, base_type) {
|
||||
if is_temporal_expression(default_value) {
|
||||
return default_value.to_string();
|
||||
return mysql_temporal_default_with_column_precision(dialect, data_type, default_value);
|
||||
}
|
||||
return quote_string(default_value);
|
||||
}
|
||||
@@ -402,6 +403,27 @@ pub(super) fn format_default_for_sql(dialect: StructureDialect, data_type: &str,
|
||||
default_value.to_string()
|
||||
}
|
||||
|
||||
/// MySQL requires a `CURRENT_TIMESTAMP` default on a fractional-precision
|
||||
/// temporal column to spell out the column's precision (`datetime(3)` needs
|
||||
/// `DEFAULT CURRENT_TIMESTAMP(3)`), otherwise the statement fails with
|
||||
/// `ERROR 1067 (42000): Invalid default value`. Complete a bare
|
||||
/// `CURRENT_TIMESTAMP` with the precision parsed from the data type; a value
|
||||
/// that already carries parentheses is passed through unchanged.
|
||||
fn mysql_temporal_default_with_column_precision(
|
||||
dialect: StructureDialect,
|
||||
data_type: &str,
|
||||
default_value: &str,
|
||||
) -> String {
|
||||
let trimmed = default_value.trim();
|
||||
if dialect != StructureDialect::Mysql || !trimmed.eq_ignore_ascii_case("current_timestamp") {
|
||||
return default_value.to_string();
|
||||
}
|
||||
match mysql_temporal_precision(data_type) {
|
||||
Some(fsp) => format!("{trimmed}({fsp})"),
|
||||
None => default_value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn normalize_default(value: Option<&String>) -> String {
|
||||
let trimmed = value.map(|value| value.trim()).unwrap_or("");
|
||||
if trimmed.eq_ignore_ascii_case("null") {
|
||||
|
||||
Reference in new issue
Block a user