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.
This commit is contained in:
parent
479f4623db
commit
72d5435535
|
|
@ -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(
|
|||
<th class="w-16 whitespace-nowrap border-b border-r px-1.5 py-1.5 text-left">
|
||||
{{ t("structureEditor.nullable") }}
|
||||
</th>
|
||||
<th class="w-14 whitespace-nowrap border-b border-r px-1.5 py-1.5 text-center">
|
||||
{{ t("structureEditor.primaryKey") }}
|
||||
</th>
|
||||
<th class="min-w-28 border-b border-r px-1.5 py-1.5 text-left">
|
||||
{{ t("structureEditor.defaultValue") }}
|
||||
</th>
|
||||
|
|
@ -551,6 +560,15 @@ watch(
|
|||
<span>{{ column.isNullable ? t("structureEditor.yes") : t("structureEditor.no") }}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="border-b border-r px-1.5 py-1 text-center">
|
||||
<input
|
||||
v-model="column.isPrimaryKey"
|
||||
type="checkbox"
|
||||
class="h-3.5 w-3.5"
|
||||
:disabled="isPrimaryKeyDisabled(column)"
|
||||
@change="() => { if (column.isPrimaryKey) column.isNullable = false; }"
|
||||
/>
|
||||
</td>
|
||||
<td class="border-b border-r px-1.5 py-1">
|
||||
<Input
|
||||
v-model="column.defaultValue"
|
||||
|
|
@ -874,7 +892,13 @@ watch(
|
|||
|
||||
<div class="flex min-w-0 flex-col rounded-md border">
|
||||
<div class="flex items-center justify-between border-b px-2 py-1.5 text-[11px] font-medium">
|
||||
<span>{{ t("structureEditor.sqlPreview") }}</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span>{{ t("structureEditor.sqlPreview") }}</span>
|
||||
<Badge v-if="!saving && pendingStatements.length && warnings.length === 0" variant="outline" class="h-4 px-1 text-[10px]">
|
||||
<Check class="h-3 w-3" />
|
||||
{{ t("structureEditor.ready") }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
<Loader2 v-if="sqlPreviewLoading" class="h-3 w-3 animate-spin" />
|
||||
<span v-else>{{ pendingStatements.length }}</span>
|
||||
|
|
@ -921,10 +945,6 @@ watch(
|
|||
<Save v-else class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.apply") }}
|
||||
</Button>
|
||||
<Badge v-if="!saving && pendingStatements.length && warnings.length === 0" variant="outline" class="h-8">
|
||||
<Check class="h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.ready") }}
|
||||
</Badge>
|
||||
</DialogFooter>
|
||||
</DialogScrollContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -888,6 +888,7 @@ export default {
|
|||
columnName: "Column",
|
||||
dataType: "Type",
|
||||
nullable: "Nullable",
|
||||
primaryKey: "Primary Key",
|
||||
defaultValue: "Default",
|
||||
comment: "Comment",
|
||||
editComment: "Edit comment",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -869,6 +869,7 @@ export default {
|
|||
columnName: "字段名",
|
||||
dataType: "类型",
|
||||
nullable: "可为空",
|
||||
primaryKey: "主键",
|
||||
defaultValue: "默认值",
|
||||
comment: "注释",
|
||||
editComment: "编辑注释",
|
||||
|
|
|
|||
|
|
@ -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<DatabaseType>) -> 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<DatabaseType>) -> 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<Strin
|
|||
}
|
||||
}
|
||||
|
||||
// Emit primary key constraint changes after individual column changes
|
||||
statements.extend(build_primary_key_sql(options, dialect, &table, warnings));
|
||||
|
||||
statements
|
||||
}
|
||||
|
||||
fn build_primary_key_sql(
|
||||
options: &TableStructureSqlOptions,
|
||||
dialect: StructureDialect,
|
||||
table: &str,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Vec<String> {
|
||||
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::<Vec<_>>()
|
||||
.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::<String>::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::<String>::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::<String>::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::<String>::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::<String>::new());
|
||||
assert_eq!(result.warnings.len(), 1);
|
||||
assert!(result.warnings[0].contains("primary key"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue