fix(mysql): preserve generated columns in table editor
This commit is contained in:
7 files changed
+481
-7
No files matched your search
@@ -3562,6 +3562,94 @@ fn columns_sql(database: &str, table: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn generation_expressions_sql(database: &str, table: &str) -> String {
|
||||
format!(
|
||||
"SELECT COLUMN_NAME, GENERATION_EXPRESSION \
|
||||
FROM information_schema.COLUMNS \
|
||||
WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \
|
||||
AND GENERATION_EXPRESSION IS NOT NULL AND GENERATION_EXPRESSION <> '' \
|
||||
ORDER BY ORDINAL_POSITION",
|
||||
quote_value(database),
|
||||
quote_value(table),
|
||||
)
|
||||
}
|
||||
|
||||
fn mysql_generated_column_storage(extra: &str) -> Option<&'static str> {
|
||||
let normalized = extra.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
|
||||
if normalized.contains("virtual generated") {
|
||||
Some("VIRTUAL")
|
||||
} else if normalized.contains("stored generated") {
|
||||
Some("STORED")
|
||||
} else if normalized.contains("persistent generated") {
|
||||
Some("PERSISTENT")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn mysql_generated_column_extra(extra: &str, expression: &str) -> Option<String> {
|
||||
let storage = mysql_generated_column_storage(extra)?;
|
||||
let expression = expression.trim();
|
||||
if expression.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(format!("GENERATED ALWAYS AS ({expression}) {storage}"))
|
||||
}
|
||||
|
||||
async fn enrich_mysql_generated_column_expressions(
|
||||
conn: &mut mysql_async::Conn,
|
||||
database: &str,
|
||||
table: &str,
|
||||
columns: &mut [ColumnInfo],
|
||||
) {
|
||||
if database.trim().is_empty()
|
||||
|| !columns
|
||||
.iter()
|
||||
.any(|column| column.extra.as_deref().is_some_and(|extra| mysql_generated_column_storage(extra).is_some()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let sql = generation_expressions_sql(database, table);
|
||||
let rows = match conn.query_iter(&sql).await {
|
||||
Ok(result) => match result.collect_and_drop::<mysql_async::Row>().await {
|
||||
Ok(rows) => rows,
|
||||
Err(error) => {
|
||||
log::debug!("Failed to collect MySQL generated-column expressions with `{sql}`: {error}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
log::debug!("Failed to read MySQL generated-column expressions with `{sql}`: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let expressions = rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
let name = get_str_by_name(row, "COLUMN_NAME");
|
||||
let expression = get_str_by_name(row, "GENERATION_EXPRESSION");
|
||||
(!name.is_empty() && !expression.trim().is_empty()).then_some((name, expression))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
apply_mysql_generated_column_expressions(columns, &expressions);
|
||||
}
|
||||
|
||||
fn apply_mysql_generated_column_expressions(columns: &mut [ColumnInfo], expressions: &HashMap<String, String>) {
|
||||
for column in columns {
|
||||
let Some(expression) = expressions.get(&column.name) else {
|
||||
continue;
|
||||
};
|
||||
let Some(extra) = column.extra.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(generated_extra) = mysql_generated_column_extra(extra, expression) {
|
||||
column.extra = Some(generated_extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn table_collation_sql(database: &str, table: &str) -> String {
|
||||
format!(
|
||||
"SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} LIMIT 1",
|
||||
@@ -3778,6 +3866,7 @@ where
|
||||
return get_columns_show(pool, database, table).await;
|
||||
}
|
||||
|
||||
enrich_mysql_generated_column_expressions(&mut conn, database, table, &mut columns).await;
|
||||
normalize_mysql_column_charset_metadata(&mut columns, table_collation.as_deref());
|
||||
Ok(columns)
|
||||
}
|
||||
@@ -3834,6 +3923,7 @@ where
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
enrich_mysql_generated_column_expressions(&mut conn, database, table, &mut columns).await;
|
||||
normalize_mysql_column_charset_metadata(&mut columns, table_collation.as_deref());
|
||||
Ok(columns)
|
||||
}
|
||||
@@ -6603,6 +6693,57 @@ mod tests {
|
||||
assert!(!sql.contains("AS ENUM_VALUES"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_generation_expression_sql_is_separate_and_scoped() {
|
||||
let sql = generation_expressions_sql("app", "products");
|
||||
|
||||
assert!(sql.contains("COLUMN_NAME, GENERATION_EXPRESSION"));
|
||||
assert!(sql.contains("TABLE_SCHEMA = 'app'"));
|
||||
assert!(sql.contains("TABLE_NAME = 'products'"));
|
||||
assert!(sql.contains("GENERATION_EXPRESSION <> ''"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_generated_column_extra_rebuilds_virtual_and_stored_clauses() {
|
||||
assert_eq!(
|
||||
mysql_generated_column_extra("STORED GENERATED", "`price` * `quantity`"),
|
||||
Some("GENERATED ALWAYS AS (`price` * `quantity`) STORED".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_generated_column_extra("VIRTUAL GENERATED", "lower(`name`)"),
|
||||
Some("GENERATED ALWAYS AS (lower(`name`)) VIRTUAL".to_string())
|
||||
);
|
||||
assert_eq!(mysql_generated_column_extra("DEFAULT_GENERATED", "current_timestamp()"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_generated_column_expressions_enrich_only_generated_columns() {
|
||||
let mut columns = vec![
|
||||
ColumnInfo { name: "total".to_string(), extra: Some("STORED GENERATED".to_string()), ..Default::default() },
|
||||
ColumnInfo {
|
||||
name: "search_name".to_string(),
|
||||
extra: Some("VIRTUAL GENERATED".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ColumnInfo {
|
||||
name: "created_at".to_string(),
|
||||
extra: Some("DEFAULT_GENERATED".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let expressions = HashMap::from([
|
||||
("total".to_string(), "`price` * `quantity`".to_string()),
|
||||
("search_name".to_string(), "lower(`name`)".to_string()),
|
||||
("created_at".to_string(), "current_timestamp()".to_string()),
|
||||
]);
|
||||
|
||||
apply_mysql_generated_column_expressions(&mut columns, &expressions);
|
||||
|
||||
assert_eq!(columns[0].extra.as_deref(), Some("GENERATED ALWAYS AS (`price` * `quantity`) STORED"));
|
||||
assert_eq!(columns[1].extra.as_deref(), Some("GENERATED ALWAYS AS (lower(`name`)) VIRTUAL"));
|
||||
assert_eq!(columns[2].extra.as_deref(), Some("DEFAULT_GENERATED"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_nullable_table_collation_uses_optional_string_conversion() {
|
||||
let collation = mysql_async::from_value_opt::<Option<String>>(mysql_async::Value::NULL)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::column_format::{clickhouse_column_type, column_data_type, column_definition, is_mysql_character_data_type};
|
||||
use super::column_format::{
|
||||
clickhouse_column_type, column_data_type, column_definition, is_mysql_character_data_type,
|
||||
original_is_mysql_generated_column, original_mysql_generated_clause,
|
||||
};
|
||||
use super::columns::build_drop_column_sql;
|
||||
use super::comments::build_sqlserver_column_comment_sql;
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
@@ -79,6 +82,16 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
|
||||
{
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
if dialect == StructureDialect::Mysql
|
||||
&& original_is_mysql_generated_column(&options.column)
|
||||
&& original_mysql_generated_clause(&options.column).is_none()
|
||||
{
|
||||
warnings.push(format!(
|
||||
"Column \"{}\" is generated, but its generation expression could not be loaded; no ALTER statement was generated to avoid removing the generated-column definition.",
|
||||
original.name
|
||||
));
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
if !has_rename && !has_attribute_change && !has_column_extra_change(&options.column) {
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
@@ -130,6 +143,14 @@ fn is_column_extra_empty(extra: &ColumnExtra) -> bool {
|
||||
|
||||
fn original_manticore_extra_flags(extra: &str) -> (bool, bool, bool, bool) {
|
||||
let lower = extra.to_lowercase();
|
||||
let mut tokens = lower.split_whitespace();
|
||||
let first = tokens.next();
|
||||
let is_generated_column = first.is_some_and(|token| token == "generated")
|
||||
|| matches!(first, Some("virtual" | "stored" | "persistent"))
|
||||
&& tokens.next().is_some_and(|token| token == "generated");
|
||||
if is_generated_column {
|
||||
return (false, false, false, false);
|
||||
}
|
||||
(
|
||||
lower.split_whitespace().any(|token| token == "indexed"),
|
||||
lower.split_whitespace().any(|token| token == "stored"),
|
||||
|
||||
@@ -19,19 +19,28 @@ pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStru
|
||||
parts.push(format!("COLLATE {}", quote_ident(dialect, &column.collation)));
|
||||
}
|
||||
}
|
||||
let mysql_generated_clause =
|
||||
(dialect == StructureDialect::Mysql).then(|| original_mysql_generated_clause(column)).flatten();
|
||||
if let Some(generated_clause) = mysql_generated_clause.as_ref() {
|
||||
parts.push(generated_clause.clone());
|
||||
}
|
||||
if !column.is_nullable && !is_oracle_like(dialect) && dialect != StructureDialect::ClickHouse {
|
||||
parts.push("NOT NULL".to_string());
|
||||
}
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
parts.push(extra_clause);
|
||||
if mysql_generated_clause.is_none() {
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
parts.push(extra_clause);
|
||||
}
|
||||
}
|
||||
let default_value = normalize_default(Some(&column.default_value));
|
||||
if !default_value.is_empty() {
|
||||
if mysql_generated_clause.is_none() && !default_value.is_empty() {
|
||||
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
|
||||
}
|
||||
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());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(dialect, StructureDialect::Mysql | StructureDialect::Doris) && !clean(&column.comment).is_empty() {
|
||||
@@ -40,6 +49,25 @@ pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStru
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
pub(super) fn original_mysql_generated_clause(column: &EditableStructureColumn) -> Option<String> {
|
||||
let extra = column.original.as_ref()?.extra.as_deref()?.trim();
|
||||
let normalized = extra.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
|
||||
(normalized.starts_with("generated ") && normalized.contains(" as (")).then(|| extra.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn original_is_mysql_generated_column(column: &EditableStructureColumn) -> bool {
|
||||
let Some(extra) = column.original.as_ref().and_then(|original| original.extra.as_deref()) else {
|
||||
return false;
|
||||
};
|
||||
let mut tokens = extra.split_whitespace();
|
||||
let first = tokens.next();
|
||||
first.is_some_and(|token| token.eq_ignore_ascii_case("generated"))
|
||||
|| matches!(first, Some(token) if token.eq_ignore_ascii_case("virtual")
|
||||
|| token.eq_ignore_ascii_case("stored")
|
||||
|| token.eq_ignore_ascii_case("persistent"))
|
||||
&& tokens.next().is_some_and(|token| token.eq_ignore_ascii_case("generated"))
|
||||
}
|
||||
|
||||
pub(super) fn column_extra_clause(dialect: StructureDialect, column: &EditableStructureColumn) -> Option<String> {
|
||||
let extra = column.extra.as_ref()?;
|
||||
match dialect {
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::column_alter::{
|
||||
};
|
||||
use super::column_format::{
|
||||
column_definition, has_dameng_identity, is_dameng_identity_compatible_type, is_mysql_character_data_type,
|
||||
original_is_mysql_generated_column, original_mysql_generated_clause,
|
||||
};
|
||||
use super::comments::build_sqlserver_column_comment_sql;
|
||||
use super::dialect::{capabilities_for, database_label, is_oracle_like, StructureDialect};
|
||||
@@ -165,6 +166,16 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if dialect == StructureDialect::Mysql
|
||||
&& original_is_mysql_generated_column(column)
|
||||
&& original_mysql_generated_clause(column).is_none()
|
||||
{
|
||||
warnings.push(format!(
|
||||
"Column \"{}\" is generated, but its generation expression could not be loaded; no ALTER statement was generated to avoid removing the generated-column definition.",
|
||||
original.name
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if !has_rename && !has_attribute_change && !has_position_change {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3838,6 +3838,34 @@ fn mysql_single_column_alter_quotes_datetime_literal() {
|
||||
assert!(result.statements.iter().any(|s| s.contains("DEFAULT '2024-01-01 00:00:00'")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_single_generated_column_change_is_blocked_without_expression_metadata() {
|
||||
let mut generated = column("total");
|
||||
generated.data_type = "decimal(14,2)".to_string();
|
||||
generated.extra = Some(ColumnExtra::default());
|
||||
generated.original = Some(ColumnInfo {
|
||||
name: "total".to_string(),
|
||||
data_type: "decimal(12,2)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: Some("STORED GENERATED".to_string()),
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_single_column_alter_sql(SingleColumnAlterSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "products".to_string(),
|
||||
column: generated,
|
||||
});
|
||||
|
||||
assert!(result.statements.is_empty());
|
||||
assert_eq!(result.warnings.len(), 1);
|
||||
assert!(result.warnings[0].contains("generation expression could not be loaded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_foreign_key_changes() {
|
||||
let mut existing = foreign_key("fk_orders_users", "user_id", "users", "id");
|
||||
@@ -4582,6 +4610,116 @@ fn mysql_character_column_preserves_charset_collation_on_other_change() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_generated_column_preserves_expression_when_modified() {
|
||||
let mut generated = column("total");
|
||||
generated.data_type = "decimal(14,2)".to_string();
|
||||
generated.is_nullable = false;
|
||||
generated.comment = "Computed total".to_string();
|
||||
generated.extra = Some(ColumnExtra::default());
|
||||
generated.original = Some(ColumnInfo {
|
||||
name: "total".to_string(),
|
||||
data_type: "decimal(12,2)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: Some("GENERATED ALWAYS AS (`price` * `quantity`) STORED".to_string()),
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
generated.original_position = Some(0);
|
||||
|
||||
let result = build_table_structure_change_sql(structure_change_options(
|
||||
DatabaseType::Mysql,
|
||||
None,
|
||||
"products",
|
||||
vec![generated],
|
||||
));
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"ALTER TABLE `products` MODIFY COLUMN `total` decimal(14,2) GENERATED ALWAYS AS (`price` * `quantity`) STORED NOT NULL COMMENT 'Computed total';"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_unchanged_generated_column_is_not_modified_with_other_columns() {
|
||||
let mut generated = column("total");
|
||||
generated.data_type = "decimal(12,2)".to_string();
|
||||
generated.extra = Some(ColumnExtra::default());
|
||||
generated.original = Some(ColumnInfo {
|
||||
name: "total".to_string(),
|
||||
data_type: "decimal(12,2)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: Some("STORED GENERATED".to_string()),
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
generated.original_position = Some(0);
|
||||
|
||||
let mut status = column("status");
|
||||
status.data_type = "varchar(50)".to_string();
|
||||
status.comment = "状态1".to_string();
|
||||
status.original = Some(ColumnInfo {
|
||||
name: "status".to_string(),
|
||||
data_type: "varchar(50)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
status.original_position = Some(1);
|
||||
|
||||
let result = build_table_structure_change_sql(structure_change_options(
|
||||
DatabaseType::Mysql,
|
||||
None,
|
||||
"product_info",
|
||||
vec![generated, status],
|
||||
));
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec!["ALTER TABLE `product_info` MODIFY COLUMN `status` varchar(50) COMMENT '状态1';"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_generated_column_change_is_blocked_without_expression_metadata() {
|
||||
let mut generated = column("total");
|
||||
generated.data_type = "decimal(14,2)".to_string();
|
||||
generated.extra = Some(ColumnExtra::default());
|
||||
generated.original = Some(ColumnInfo {
|
||||
name: "total".to_string(),
|
||||
data_type: "decimal(12,2)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: Some("STORED GENERATED".to_string()),
|
||||
comment: None,
|
||||
..Default::default()
|
||||
});
|
||||
generated.original_position = Some(0);
|
||||
|
||||
let result = build_table_structure_change_sql(structure_change_options(
|
||||
DatabaseType::Mysql,
|
||||
None,
|
||||
"products",
|
||||
vec![generated],
|
||||
));
|
||||
|
||||
assert!(result.statements.is_empty());
|
||||
assert_eq!(result.warnings.len(), 1);
|
||||
assert!(result.warnings[0].contains("generation expression could not be loaded"));
|
||||
}
|
||||
|
||||
// ---- Oscar (神通) ----
|
||||
// 神通 v7 是 Oracle 兼容方言,且实测支持 ALTER TABLE DROP/ADD PRIMARY KEY(与 Dameng 一致,
|
||||
// 不同于 Oracle)。DDL 生成走 StructureDialect::Oscar,与 Dameng 共享 Oracle-like 分支。
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use dbx_core::db::mysql;
|
||||
use dbx_core::models::connection::DatabaseType;
|
||||
use dbx_core::table_structure_sql::{
|
||||
build_table_structure_change_sql, ColumnExtra, ColumnInfo as StructureColumnInfo, EditableStructureColumn,
|
||||
TableStructureSqlOptions,
|
||||
};
|
||||
use dbx_core::types::ColumnInfo;
|
||||
use mysql_async::prelude::Queryable;
|
||||
|
||||
fn editable_column(column: &ColumnInfo, index: usize) -> EditableStructureColumn {
|
||||
EditableStructureColumn {
|
||||
id: format!("existing:{}", column.name),
|
||||
name: column.name.clone(),
|
||||
data_type: column.data_type.clone(),
|
||||
is_nullable: column.is_nullable,
|
||||
default_value: column.column_default.clone().unwrap_or_default(),
|
||||
comment: column.comment.clone().unwrap_or_default(),
|
||||
is_primary_key: column.is_primary_key,
|
||||
extra: Some(ColumnExtra::default()),
|
||||
original: Some(StructureColumnInfo {
|
||||
name: column.name.clone(),
|
||||
data_type: column.data_type.clone(),
|
||||
is_nullable: column.is_nullable,
|
||||
column_default: column.column_default.clone(),
|
||||
is_primary_key: column.is_primary_key,
|
||||
extra: column.extra.clone(),
|
||||
comment: column.comment.clone(),
|
||||
character_set: column.character_set.clone(),
|
||||
collation: column.collation.clone(),
|
||||
}),
|
||||
original_position: Some(index),
|
||||
marked_for_drop: false,
|
||||
character_set: column.character_set.clone().unwrap_or_default(),
|
||||
collation: column.collation.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn change_options(table_name: &str, columns: Vec<EditableStructureColumn>) -> TableStructureSqlOptions {
|
||||
TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: table_name.to_string(),
|
||||
columns,
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
partitioned: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn generated_extra(columns: &[ColumnInfo]) -> &str {
|
||||
columns
|
||||
.iter()
|
||||
.find(|column| column.name == "total")
|
||||
.and_then(|column| column.extra.as_deref())
|
||||
.expect("generated column metadata")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_LIVE_MYSQL_GENERATED_URL and DBX_LIVE_MYSQL_GENERATED_DATABASE"]
|
||||
async fn live_mysql_generated_column_metadata_and_alter_preserve_expression() {
|
||||
let url = std::env::var("DBX_LIVE_MYSQL_GENERATED_URL").expect("DBX_LIVE_MYSQL_GENERATED_URL");
|
||||
let database = std::env::var("DBX_LIVE_MYSQL_GENERATED_DATABASE").expect("DBX_LIVE_MYSQL_GENERATED_DATABASE");
|
||||
let table = format!("dbx_generated_{}", uuid::Uuid::new_v4().simple());
|
||||
let pool = mysql::connect(&url, std::time::Duration::from_secs(10)).await.unwrap();
|
||||
let mut conn = mysql::get_conn_with_health_check(&pool).await.unwrap();
|
||||
conn.query_drop(format!(
|
||||
"CREATE TABLE `{table}` (\
|
||||
`price` DECIMAL(10,2) NOT NULL, \
|
||||
`quantity` INT NOT NULL, \
|
||||
`total` DECIMAL(12,2) GENERATED ALWAYS AS (`price` * `quantity`) STORED, \
|
||||
`status` VARCHAR(50) NULL)"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let columns = mysql::get_columns(&pool, &database, &table).await.unwrap();
|
||||
let extra = generated_extra(&columns);
|
||||
assert!(extra.starts_with("GENERATED ALWAYS AS ("), "extra: {extra}");
|
||||
assert!(extra.contains("`price` * `quantity`"), "extra: {extra}");
|
||||
assert!(extra.ends_with(" STORED"), "extra: {extra}");
|
||||
|
||||
let mut drafts =
|
||||
columns.iter().enumerate().map(|(index, column)| editable_column(column, index)).collect::<Vec<_>>();
|
||||
drafts.iter_mut().find(|column| column.name == "status").unwrap().comment = "状态1".to_string();
|
||||
let status_change = build_table_structure_change_sql(change_options(&table, drafts));
|
||||
assert_eq!(status_change.warnings, Vec::<String>::new());
|
||||
assert_eq!(status_change.statements.len(), 1, "statements: {:?}", status_change.statements);
|
||||
assert!(status_change.statements[0].contains("MODIFY COLUMN `status`"));
|
||||
assert!(!status_change.statements[0].contains("`total`"));
|
||||
conn.query_drop(&status_change.statements[0]).await.unwrap();
|
||||
|
||||
let columns = mysql::get_columns(&pool, &database, &table).await.unwrap();
|
||||
let mut drafts =
|
||||
columns.iter().enumerate().map(|(index, column)| editable_column(column, index)).collect::<Vec<_>>();
|
||||
drafts.iter_mut().find(|column| column.name == "total").unwrap().data_type = "decimal(14,2)".to_string();
|
||||
let generated_change = build_table_structure_change_sql(change_options(&table, drafts));
|
||||
assert_eq!(generated_change.warnings, Vec::<String>::new());
|
||||
assert_eq!(generated_change.statements.len(), 1, "statements: {:?}", generated_change.statements);
|
||||
assert!(generated_change.statements[0].contains("GENERATED ALWAYS AS ("));
|
||||
assert!(generated_change.statements[0].contains("`price` * `quantity`"));
|
||||
assert!(generated_change.statements[0].contains(") STORED"));
|
||||
conn.query_drop(&generated_change.statements[0]).await.unwrap();
|
||||
|
||||
let columns = mysql::get_columns(&pool, &database, &table).await.unwrap();
|
||||
assert_eq!(columns.iter().find(|column| column.name == "total").unwrap().data_type, "decimal(14,2)");
|
||||
assert!(generated_extra(&columns).contains("`price` * `quantity`"));
|
||||
|
||||
conn.query_drop(format!("DROP TABLE `{table}`")).await.unwrap();
|
||||
drop(conn);
|
||||
pool.disconnect().await.unwrap();
|
||||
}
|
||||
Reference in new issue
Block a user