feat(editor): add table copy/paste keyboard shortcuts

* feat: add table copy/paste with configurable structure and data options

- Implement Navicat-style table copy/paste functionality
- Support multi-table selection and batch operations
- Configurable paste modes: structure+data (default), structure only, data only
- Backend: add build_copy_table_data_sql API (INSERT INTO ... SELECT)
- TreeItem.vue: copy/paste context menus, paste dialog with mode selection
- ObjectBrowser.vue: batch toolbar buttons, context menu, paste dialog
- Refactor TreeClipboard type to support multi-table entries
- Add i18n keys for all 7 locale files (en, zh-CN, zh-TW, ja, es, it, pt-BR)

* Fix table paste shortcuts

* Fix Kafka MQ sidebar detection

* Restrict table paste scope

* Support data copy for primary SQL databases
This commit is contained in:
miracle authored and GitHub committed 2026-07-01 12:49:37 +08:00
1 parent 5d2ce9f189
commit 0c91fecf5d
26 files changed
+943 -34

No files matched your search

+104
View File
@@ -116,6 +116,23 @@ pub struct DuplicateTableStructureSqlOptions {
pub target_name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CopyTableDataSqlOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub source_name: String,
pub target_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<String>>,
#[serde(default)]
pub postgres_overriding_system_value: bool,
#[serde(default)]
pub sqlserver_identity_insert: bool,
}
const MYSQL_COMPATIBLE_PROFILES: &[&str] =
&["mysql", "mariadb", "tidb", "oceanbase", "doris", "starrocks", "custom_mysql"];
@@ -309,6 +326,32 @@ pub fn build_duplicate_table_structure_sql(options: DuplicateTableStructureSqlOp
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 0;")
}
pub fn build_copy_table_data_sql(options: CopyTableDataSqlOptions) -> String {
let source = qualified_name(options.database_type, options.schema.as_deref(), &options.source_name);
let target = qualified_name(options.database_type, options.schema.as_deref(), &options.target_name);
let Some(columns) = options.columns.filter(|columns| !columns.is_empty()) else {
return format!("INSERT INTO {target} SELECT * FROM {source};");
};
let column_list = columns
.iter()
.map(|column| quote_table_identifier(options.database_type, column))
.collect::<Vec<_>>()
.join(", ");
let postgres_override = if options.postgres_overriding_system_value
&& matches!(options.database_type, Some(DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb))
{
" OVERRIDING SYSTEM VALUE"
} else {
""
};
let insert_sql =
format!("INSERT INTO {target} ({column_list}){postgres_override} SELECT {column_list} FROM {source};");
if options.sqlserver_identity_insert && options.database_type == Some(DatabaseType::SqlServer) {
return format!("SET IDENTITY_INSERT {target} ON;\n{insert_sql}\nSET IDENTITY_INSERT {target} OFF;");
}
insert_sql
}
pub fn supports_object_rename(database_type: Option<DatabaseType>, object_type: DatabaseObjectType) -> bool {
let Some(database_type) = database_type else {
return false;
@@ -766,6 +809,15 @@ mod tests {
}),
"CREATE TABLE `users_copy` LIKE `users`;"
);
assert_eq!(
build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions {
database_type: Some(DatabaseType::Postgres),
schema: Some("public".to_string()),
source_name: "users".to_string(),
target_name: "users_copy".to_string(),
}),
"CREATE TABLE \"public\".\"users_copy\" (LIKE \"public\".\"users\" INCLUDING ALL);"
);
assert_eq!(
build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions {
database_type: Some(DatabaseType::Kwdb),
@@ -813,6 +865,58 @@ mod tests {
);
}
#[test]
fn builds_copy_table_data_sql() {
assert_eq!(
build_copy_table_data_sql(CopyTableDataSqlOptions {
database_type: Some(DatabaseType::Sqlite),
schema: None,
source_name: "users".to_string(),
target_name: "users_copy".to_string(),
columns: None,
postgres_overriding_system_value: false,
sqlserver_identity_insert: false,
}),
"INSERT INTO \"users_copy\" SELECT * FROM \"users\";"
);
assert_eq!(
build_copy_table_data_sql(CopyTableDataSqlOptions {
database_type: Some(DatabaseType::Mysql),
schema: None,
source_name: "users".to_string(),
target_name: "users_copy".to_string(),
columns: Some(vec!["id".to_string(), "name".to_string()]),
postgres_overriding_system_value: false,
sqlserver_identity_insert: false,
}),
"INSERT INTO `users_copy` (`id`, `name`) SELECT `id`, `name` FROM `users`;"
);
assert_eq!(
build_copy_table_data_sql(CopyTableDataSqlOptions {
database_type: Some(DatabaseType::Postgres),
schema: Some("public".to_string()),
source_name: "users".to_string(),
target_name: "users_copy".to_string(),
columns: Some(vec!["id".to_string(), "name".to_string()]),
postgres_overriding_system_value: true,
sqlserver_identity_insert: false,
}),
"INSERT INTO \"public\".\"users_copy\" (\"id\", \"name\") OVERRIDING SYSTEM VALUE SELECT \"id\", \"name\" FROM \"public\".\"users\";"
);
assert_eq!(
build_copy_table_data_sql(CopyTableDataSqlOptions {
database_type: Some(DatabaseType::SqlServer),
schema: Some("dbo".to_string()),
source_name: "users".to_string(),
target_name: "users_copy".to_string(),
columns: Some(vec!["id".to_string(), "name".to_string()]),
postgres_overriding_system_value: false,
sqlserver_identity_insert: true,
}),
"SET IDENTITY_INSERT [dbo].[users_copy] ON;\nINSERT INTO [dbo].[users_copy] ([id], [name]) SELECT [id], [name] FROM [dbo].[users];\nSET IDENTITY_INSERT [dbo].[users_copy] OFF;"
);
}
#[test]
fn builds_mysql_table_and_view_rename_sql() {
assert_eq!(