From 72d5435535313a06ac6fec9b76e9bebdbffeb7a4 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 24 May 2026 20:32:30 +0800 Subject: [PATCH] feat: add primary key editing to table structure editor Add PK checkbox column in Vue dialog, alter_primary_key capability flag, build_primary_key_sql() for PostgreSQL/MySQL ALTER TABLE PK changes, and i18n labels in zh-CN/en/es. Includes flicker fix (silent reload after apply) and move ready badge to SQL preview header. --- .../structure/TableStructureEditorDialog.vue | 38 ++- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + crates/dbx-core/src/table_structure_sql.rs | 241 ++++++++++++++++++ 5 files changed, 273 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/structure/TableStructureEditorDialog.vue b/apps/desktop/src/components/structure/TableStructureEditorDialog.vue index 8c9bb558d..5f302b732 100644 --- a/apps/desktop/src/components/structure/TableStructureEditorDialog.vue +++ b/apps/desktop/src/components/structure/TableStructureEditorDialog.vue @@ -188,9 +188,9 @@ function resetState() { newTableName.value = ""; } -async function loadStructure() { +async function loadStructure(silent = false) { if (!props.prefillConnectionId || !props.prefillDatabase || !props.prefillTable) return; - loading.value = true; + if (!silent) loading.value = true; errorMessage.value = ""; try { await store.ensureConnected(props.prefillConnectionId); @@ -218,7 +218,7 @@ async function loadStructure() { } catch (e: any) { errorMessage.value = e?.message || String(e); } finally { - loading.value = false; + if (!silent) loading.value = false; } } @@ -282,6 +282,12 @@ function isColumnCommentDisabled(column: EditableStructureColumn): boolean { return column.markedForDrop || !structureCapabilities.value.comment; } +function isPrimaryKeyDisabled(column: EditableStructureColumn): boolean { + if (column.markedForDrop) return true; + if (!column.original) return false; + return !structureCapabilities.value.alterPrimaryKey; +} + function canDropColumn(column: EditableStructureColumn): boolean { return !!column.original && !column.isPrimaryKey && structureCapabilities.value.dropColumn; } @@ -371,7 +377,7 @@ async function applyChanges() { if (isCreateMode.value) { open.value = false; } else { - await loadStructure(); + await loadStructure(true); } } catch (e: any) { errorMessage.value = e?.message || String(e); @@ -503,6 +509,9 @@ watch( {{ t("structureEditor.nullable") }} + + {{ t("structureEditor.primaryKey") }} + {{ t("structureEditor.defaultValue") }} @@ -551,6 +560,15 @@ watch( {{ column.isNullable ? t("structureEditor.yes") : t("structureEditor.no") }} + + +
- {{ t("structureEditor.sqlPreview") }} +
+ {{ t("structureEditor.sqlPreview") }} + + + {{ t("structureEditor.ready") }} + +
{{ pendingStatements.length }} @@ -921,10 +945,6 @@ watch( {{ t("structureEditor.apply") }} - - - {{ t("structureEditor.ready") }} - diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index a353a7f18..b94b74139 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -888,6 +888,7 @@ export default { columnName: "Column", dataType: "Type", nullable: "Nullable", + primaryKey: "Primary Key", defaultValue: "Default", comment: "Comment", editComment: "Edit comment", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index ccd6c1ef6..3bd30a8d6 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -785,6 +785,7 @@ export default { columnName: "Columna", dataType: "Tipo", nullable: "Admite nulos", + primaryKey: "Clave primaria", defaultValue: "Valor por defecto", comment: "Comentario", editComment: "Editar comentario", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 6631e63fc..ca0b79683 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -869,6 +869,7 @@ export default { columnName: "字段名", dataType: "类型", nullable: "可为空", + primaryKey: "主键", defaultValue: "默认值", comment: "注释", editComment: "编辑注释", diff --git a/crates/dbx-core/src/table_structure_sql.rs b/crates/dbx-core/src/table_structure_sql.rs index 72cbffdc8..d280ad4a6 100644 --- a/crates/dbx-core/src/table_structure_sql.rs +++ b/crates/dbx-core/src/table_structure_sql.rs @@ -131,6 +131,7 @@ struct TableStructureCapabilities { index_include: bool, index_filter: bool, index_comment: bool, + alter_primary_key: bool, } impl Default for TableStructureCapabilities { @@ -150,6 +151,7 @@ impl Default for TableStructureCapabilities { index_include: false, index_filter: false, index_comment: false, + alter_primary_key: false, } } } @@ -175,6 +177,7 @@ fn capabilities_for(database_type: Option) -> TableStructureCapabi drop_index: true, rebuild_index: true, index_type: true, + alter_primary_key: true, ..base }, Some( @@ -198,6 +201,7 @@ fn capabilities_for(database_type: Option) -> TableStructureCapabi index_include: true, index_filter: true, index_comment: true, + alter_primary_key: true, ..base }, Some(DatabaseType::Redshift) => TableStructureCapabilities { @@ -469,6 +473,74 @@ fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mut Vec, +) -> Vec { + let capabilities = capabilities_for(options.database_type); + + let old_pk_names: Vec<&str> = options + .columns + .iter() + .filter(|c| c.original.as_ref().is_some_and(|o| o.is_primary_key)) + .map(|c| c.name.as_str()) + .collect(); + + let new_pk_names: Vec<&str> = options + .columns + .iter() + .filter(|c| !c.marked_for_drop && c.is_primary_key) + .map(|c| c.name.as_str()) + .collect(); + + if old_pk_names == new_pk_names { + return Vec::new(); + } + + if !capabilities.alter_primary_key { + warnings.push(format!( + "Changing primary keys is not supported for {} from this editor.", + database_label(options.database_type) + )); + return Vec::new(); + } + + let mut statements = Vec::new(); + + if !old_pk_names.is_empty() { + match dialect { + StructureDialect::Postgres => { + let raw_table = options.table_name.split('.').last().unwrap_or(&options.table_name); + let pk_name = format!("{}_pkey", clean(raw_table)); + statements.push(format!( + "ALTER TABLE {table} DROP CONSTRAINT {};", + quote_ident(dialect, &pk_name) + )); + } + StructureDialect::Mysql => { + statements.push(format!("ALTER TABLE {table} DROP PRIMARY KEY;")); + } + _ => {} + } + } + + if !new_pk_names.is_empty() { + let pk_list = new_pk_names + .iter() + .map(|n| quote_ident(dialect, n)) + .collect::>() + .join(", "); + statements.push(format!("ALTER TABLE {table} ADD PRIMARY KEY ({pk_list});")); + } + statements } @@ -1492,4 +1564,173 @@ mod tests { ] ); } + + #[test] + fn builds_postgres_alter_table_add_primary_key() { + let mut id = column("id"); + id.data_type = "integer".to_string(); + id.is_nullable = false; + id.is_primary_key = true; + id.original = Some(ColumnInfo { + name: "id".to_string(), + data_type: "integer".to_string(), + is_nullable: false, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("public".to_string()), + table_name: "users".to_string(), + columns: vec![id], + indexes: Vec::new(), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["ALTER TABLE \"public\".\"users\" ADD PRIMARY KEY (\"id\");"] + ); + } + + #[test] + fn builds_postgres_alter_table_drop_primary_key() { + let mut id = column("id"); + id.data_type = "integer".to_string(); + id.is_nullable = false; + id.is_primary_key = false; + id.original = Some(ColumnInfo { + name: "id".to_string(), + data_type: "integer".to_string(), + is_nullable: false, + column_default: None, + is_primary_key: true, + extra: None, + comment: None, + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: Some("public".to_string()), + table_name: "users".to_string(), + columns: vec![id], + indexes: Vec::new(), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["ALTER TABLE \"public\".\"users\" DROP CONSTRAINT \"users_pkey\";"] + ); + } + + #[test] + fn builds_mysql_alter_table_change_primary_key() { + let mut old_pk = column("id"); + old_pk.id = "old_id".to_string(); + old_pk.data_type = "int".to_string(); + old_pk.is_nullable = false; + old_pk.is_primary_key = false; + old_pk.original = Some(ColumnInfo { + name: "id".to_string(), + data_type: "int".to_string(), + is_nullable: false, + column_default: None, + is_primary_key: true, + extra: None, + comment: None, + }); + + let mut new_pk = column("uuid"); + new_pk.id = "new_uuid".to_string(); + new_pk.data_type = "varchar(36)".to_string(); + new_pk.is_nullable = false; + new_pk.is_primary_key = true; + new_pk.original = Some(ColumnInfo { + name: "uuid".to_string(), + data_type: "varchar(36)".to_string(), + is_nullable: false, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![old_pk, new_pk], + indexes: Vec::new(), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "ALTER TABLE `users` DROP PRIMARY KEY;", + "ALTER TABLE `users` ADD PRIMARY KEY (`uuid`);", + ] + ); + } + + #[test] + fn builds_no_statements_when_primary_key_unchanged() { + let mut id = column("id"); + id.data_type = "integer".to_string(); + id.is_nullable = false; + id.is_primary_key = true; + id.original = Some(ColumnInfo { + name: "id".to_string(), + data_type: "integer".to_string(), + is_nullable: false, + column_default: None, + is_primary_key: true, + extra: None, + comment: None, + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Postgres), + schema: None, + table_name: "users".to_string(), + columns: vec![id], + indexes: Vec::new(), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert!(result.statements.is_empty()); + } + + #[test] + fn warns_sqlite_cannot_alter_primary_key() { + let mut id = column("id"); + id.data_type = "integer".to_string(); + id.is_nullable = false; + id.is_primary_key = true; + id.original = Some(ColumnInfo { + name: "id".to_string(), + data_type: "integer".to_string(), + is_nullable: false, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Sqlite), + schema: None, + table_name: "users".to_string(), + columns: vec![id], + indexes: Vec::new(), + }); + + assert_eq!(result.statements, Vec::::new()); + assert_eq!(result.warnings.len(), 1); + assert!(result.warnings[0].contains("primary key")); + } }