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") }}
+
+ { if (column.isPrimaryKey) column.isNullable = false; }"
+ />
+ |
- {{ 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"));
+ }
}
|