fix(diff): sync MySQL comment differences

This commit is contained in:
t8y2
2026-06-02 10:27:08 +08:00
parent ad9b7e5908
commit ca0153acbb
7 changed files with 152 additions and 19 deletions
@@ -54,6 +54,7 @@ const executedCount = ref(0);
const executeTotal = ref(0);
const syncErrors = ref<{ sql: string; error: string }[]>([]);
const syncSql = ref("");
const ignoreComments = ref(false);
const highlightedSyncSql = computed(() => highlight(syncSql.value));
const allSelected = computed(() => diffs.value.length > 0 && diffs.value.every((d) => d.selected));
@@ -185,6 +186,7 @@ async function startCompare() {
targetDetails,
databaseType: targetConfig?.db_type || "mysql",
targetSchema: targetSchema.value,
ignoreComments: ignoreComments.value,
});
diffs.value = result.diffs.map((diff) => ({ ...diff, selected: true }));
@@ -346,6 +348,7 @@ watch(targetDatabase, (database) => {
});
watch(sourceSchema, () => resetResult());
watch(targetSchema, () => resetResult());
watch(ignoreComments, () => resetResult());
watch(
open,
@@ -495,6 +498,13 @@ watch(
</div>
</div>
<div class="flex items-center gap-2 rounded-md border bg-muted/20 px-3 py-2">
<input id="schema-diff-ignore-comments" v-model="ignoreComments" type="checkbox" class="accent-primary" />
<Label for="schema-diff-ignore-comments" class="cursor-pointer text-xs">
{{ t("diff.ignoreComments") }}
</Label>
</div>
<!-- Comparing -->
<div v-if="step === 'comparing'" class="flex items-center justify-center py-8 text-sm text-muted-foreground">
<Loader2 class="w-4 h-4 animate-spin mr-2" />
@@ -604,6 +614,13 @@ watch(
<span v-if="ti < d.triggers!.length - 1">, </span>
</span>
</template>
<template v-if="d.type === 'modified' && d.sourceTableComment !== undefined">
<span
v-if="d.columns?.length || d.indexes?.length || d.foreignKeys?.length || d.triggers?.length"
>;
</span>
<span>{{ t("diff.comments") }}</span>
</template>
<span v-else-if="d.type === 'added'" class="text-green-500">{{ t("diff.newTable") }}</span>
<span v-else-if="d.type === 'removed'" class="text-red-500">{{ t("diff.dropTable") }}</span>
</td>
+2
View File
@@ -1454,6 +1454,7 @@ export default {
selectConnection: "Select connection",
selectDatabase: "Select database",
selectSchema: "Select schema",
ignoreComments: "Ignore comment differences",
compare: "Compare",
comparing: "Comparing schemas...",
noDifferences: "No differences found",
@@ -1466,6 +1467,7 @@ export default {
indexes: "Indexes",
foreignKeys: "Foreign keys",
triggers: "Triggers",
comments: "Comments",
newTable: "New table",
dropTable: "Drop table",
generatedSql: "Sync SQL",
+2
View File
@@ -1344,6 +1344,7 @@ export default {
selectConnection: "Seleccionar conexión",
selectDatabase: "Seleccionar base de datos",
selectSchema: "Seleccionar esquema",
ignoreComments: "Ignorar diferencias de comentarios",
compare: "Comparar",
comparing: "Comparando esquemas...",
noDifferences: "No se encontraron diferencias",
@@ -1356,6 +1357,7 @@ export default {
indexes: "Índices",
foreignKeys: "Claves foráneas",
triggers: "Disparadores",
comments: "Comentarios",
newTable: "Nueva tabla",
dropTable: "Eliminar tabla",
generatedSql: "SQL de sincronización",
+2
View File
@@ -1428,6 +1428,7 @@ export default {
selectConnection: "选择连接",
selectDatabase: "选择数据库",
selectSchema: "选择 Schema",
ignoreComments: "忽略注释差异",
compare: "开始比较",
comparing: "正在比较结构...",
noDifferences: "两个数据库结构完全一致",
@@ -1440,6 +1441,7 @@ export default {
indexes: "索引",
foreignKeys: "外键",
triggers: "触发器",
comments: "注释",
newTable: "新增表",
dropTable: "删除表",
generatedSql: "同步 SQL",
+2
View File
@@ -1428,6 +1428,7 @@ export default {
selectConnection: "選擇連線",
selectDatabase: "選擇資料庫",
selectSchema: "選擇 Schema",
ignoreComments: "忽略註解差異",
compare: "開始比較",
comparing: "正在比較結構……",
noDifferences: "兩個資料庫結構完全一致",
@@ -1440,6 +1441,7 @@ export default {
indexes: "索引",
foreignKeys: "外鍵",
triggers: "觸發器",
comments: "註解",
newTable: "新增資料表",
dropTable: "刪除資料表",
generatedSql: "同步 SQL",
+1
View File
@@ -61,6 +61,7 @@ export interface SchemaDiffPreparationOptions {
targetDetails: TableSchemaDetail[];
databaseType: DatabaseType;
targetSchema?: string;
ignoreComments?: boolean;
}
export interface SchemaDiffPreparation {
+126 -19
View File
@@ -115,6 +115,8 @@ pub struct SchemaDiffPreparationOptions {
pub database_type: DatabaseType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_schema: Option<String>,
#[serde(default)]
pub ignore_comments: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -232,13 +234,14 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
for name in common {
let Some(source) = source_details.get(name.as_str()) else { continue };
let Some(target) = target_details.get(name.as_str()) else { continue };
let column_diffs = diff_columns(&source.columns, &target.columns);
let column_diffs = diff_columns_with_options(&source.columns, &target.columns, options.ignore_comments);
let index_diffs = diff_indexes(&source.indexes, &target.indexes);
let foreign_key_diffs = diff_foreign_keys(&source.foreign_keys, &target.foreign_keys);
let trigger_diffs = diff_triggers(&source.triggers, &target.triggers);
let source_comment = source_table_comments.get(name.as_str()).cloned().unwrap_or(None);
let target_comment = target_table_comments.get(name.as_str()).cloned().unwrap_or(None);
let comment_changed = source_comment.clone().unwrap_or_default() != target_comment.clone().unwrap_or_default();
let comment_changed = !options.ignore_comments
&& source_comment.clone().unwrap_or_default() != target_comment.clone().unwrap_or_default();
if !column_diffs.is_empty()
|| !index_diffs.is_empty()
@@ -275,6 +278,10 @@ fn diff_names(source: &[String], target: &[String]) -> (Vec<String>, Vec<String>
}
pub fn diff_columns(source: &[ColumnInfo], target: &[ColumnInfo]) -> Vec<ColumnDiff> {
diff_columns_with_options(source, target, false)
}
fn diff_columns_with_options(source: &[ColumnInfo], target: &[ColumnInfo], ignore_comments: bool) -> Vec<ColumnDiff> {
let mut diffs = Vec::new();
let target_map: HashMap<&str, &ColumnInfo> = target.iter().map(|column| (column.name.as_str(), column)).collect();
let source_map: HashMap<&str, &ColumnInfo> = source.iter().map(|column| (column.name.as_str(), column)).collect();
@@ -301,8 +308,9 @@ pub fn diff_columns(source: &[ColumnInfo], target: &[ColumnInfo]) -> Vec<ColumnD
source_column.column_default.as_deref().unwrap_or("NULL")
));
}
if source_column.comment.as_deref().unwrap_or_default()
!= target_column.comment.as_deref().unwrap_or_default()
if !ignore_comments
&& source_column.comment.as_deref().unwrap_or_default()
!= target_column.comment.as_deref().unwrap_or_default()
{
changes.push(format!(
"comment: {} → {}",
@@ -551,6 +559,11 @@ fn column_def(col: &ColumnInfo, db_type: DatabaseType) -> String {
if let Some(default) = &col.column_default {
definition.push_str(&format!(" DEFAULT {default}"));
}
if matches!(db_type, DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks) {
if let Some(comment) = &col.comment {
definition.push_str(&format!(" COMMENT {}", comment_literal(comment)));
}
}
definition
}
@@ -755,21 +768,23 @@ pub fn generate_schema_sync_sql(diffs: &[TableDiff], db_type: DatabaseType, sche
lines.push(String::new());
}
if let Some(columns) = &diff.columns {
for column in columns {
if let Some(source) = &column.source {
if column.changes.iter().any(|change| change.starts_with("comment:")) {
lines.push(column_comment_sql(
&diff.name,
&column.name,
source.comment.as_deref().unwrap_or_default(),
db_type,
schema,
));
}
if column.diff_type == "added" {
if let Some(comment) = &source.comment {
lines.push(column_comment_sql(&diff.name, &column.name, comment, db_type, schema));
if !is_mysql {
if let Some(columns) = &diff.columns {
for column in columns {
if let Some(source) = &column.source {
if column.changes.iter().any(|change| change.starts_with("comment:")) {
lines.push(column_comment_sql(
&diff.name,
&column.name,
source.comment.as_deref().unwrap_or_default(),
db_type,
schema,
));
}
if column.diff_type == "added" {
if let Some(comment) = &source.comment {
lines.push(column_comment_sql(&diff.name, &column.name, comment, db_type, schema));
}
}
}
}
@@ -862,6 +877,21 @@ mod tests {
}
}
fn column(name: &str, data_type: &str, comment: Option<&str>) -> ColumnInfo {
ColumnInfo {
name: name.to_string(),
data_type: data_type.to_string(),
is_nullable: false,
column_default: None,
is_primary_key: false,
extra: None,
comment: comment.map(str::to_string),
numeric_precision: None,
numeric_scale: None,
character_maximum_length: None,
}
}
#[test]
fn detects_modified_indexes_not_only_added_or_removed_indexes() {
let diffs = diff_indexes(
@@ -989,6 +1019,83 @@ mod tests {
);
}
#[test]
fn mysql_column_comment_changes_generate_modify_column_sql() {
let diffs = vec![TableDiff {
diff_type: "modified".to_string(),
object_type: None,
name: "users".to_string(),
columns: Some(vec![ColumnDiff {
diff_type: "modified".to_string(),
name: "name".to_string(),
source: Some(column("name", "varchar(64)", Some("用户姓名"))),
target: Some(column("name", "varchar(64)", Some("Name"))),
changes: vec!["comment: Name → 用户姓名".to_string()],
}]),
indexes: None,
foreign_keys: None,
triggers: None,
ddl: None,
source_table_comment: Some(Some("用户表".to_string())),
target_table_comment: Some(Some("Users".to_string())),
}];
assert_eq!(
generate_schema_sync_sql(&diffs, DatabaseType::Mysql, None),
[
"-- Alter table: users",
"ALTER TABLE `users`",
" MODIFY COLUMN `name` varchar(64) NOT NULL COMMENT '用户姓名';",
"",
"ALTER TABLE `users` COMMENT = '用户表';",
]
.join("\n")
);
}
#[test]
fn ignore_comments_skips_column_and_table_comment_diffs() {
let options = SchemaDiffPreparationOptions {
source_tables: vec![TableInfo {
name: "users".to_string(),
table_type: "BASE TABLE".to_string(),
comment: Some("用户表".to_string()),
parent_schema: None,
parent_name: None,
}],
target_tables: vec![TableInfo {
name: "users".to_string(),
table_type: "BASE TABLE".to_string(),
comment: Some("Users".to_string()),
parent_schema: None,
parent_name: None,
}],
source_details: vec![TableSchemaDetail {
name: "users".to_string(),
columns: vec![column("name", "varchar(64)", Some("用户姓名"))],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
ddl: None,
}],
target_details: vec![TableSchemaDetail {
name: "users".to_string(),
columns: vec![column("name", "varchar(64)", Some("Name"))],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
ddl: None,
}],
database_type: DatabaseType::Mysql,
target_schema: None,
ignore_comments: true,
};
let result = prepare_schema_diff(options);
assert!(result.diffs.is_empty());
assert!(result.sync_sql.is_empty());
}
#[test]
fn qualifies_generated_schema_sync_sql_with_target_schema() {
let diffs = vec![TableDiff {