From 66d180f729447e818e565e39d251a175222aefcb Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Fri, 22 May 2026 11:55:08 +0800
Subject: [PATCH] feat(grid): copy insert without primary keys
---
apps/desktop/src/components/grid/DataGrid.vue | 11 ++-
.../src/composables/useDataGridExport.ts | 67 +++++++++++--------
apps/desktop/src/i18n/locales/en.ts | 2 +
apps/desktop/src/i18n/locales/es.ts | 2 +
apps/desktop/src/i18n/locales/zh-CN.ts | 2 +
apps/desktop/src/lib/dataGridSql.ts | 46 +++++++++++++
packages/app-tests/dataGridSql.test.ts | 53 +++++++++++++++
7 files changed, 152 insertions(+), 31 deletions(-)
diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue
index 81ff50b459..1728e16d16 100644
--- a/apps/desktop/src/components/grid/DataGrid.vue
+++ b/apps/desktop/src/components/grid/DataGrid.vue
@@ -2155,7 +2155,9 @@ const {
copyCell,
copyRow,
copyRowAsInsert,
+ copyRowAsInsertWithoutPrimaryKeys,
copyRowAsUpdate,
+ canCopyRowAsInsertWithoutPrimaryKeys,
canCopyRowAsUpdate,
copyAll,
copySelectionTsv,
@@ -2179,8 +2181,6 @@ const {
selectedRange,
contextCell: exportContextCell,
getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId),
- quoteIdent,
- escapeVal,
selectedRowIds,
hasRowSelection,
});
@@ -4473,6 +4473,13 @@ defineExpose({
{{ isMultiRow ? t("grid.copyRowsInsert", { count: multiRowCount }) : t("grid.copyRowInsert") }}
+
+ {{
+ isMultiRow
+ ? t("grid.copyRowsInsertWithoutPrimaryKeys", { count: multiRowCount })
+ : t("grid.copyRowInsertWithoutPrimaryKeys")
+ }}
+
{{ isMultiRow ? t("grid.copyRowsUpdate", { count: multiRowCount }) : t("grid.copyRowUpdate") }}
diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts
index edd02f3ade..e2a756a71c 100644
--- a/apps/desktop/src/composables/useDataGridExport.ts
+++ b/apps/desktop/src/composables/useDataGridExport.ts
@@ -14,7 +14,7 @@ import { useToast } from "@/composables/useToast";
import { displayCellValue, type CellValue } from "@/lib/cellValue";
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/actionActivation";
import { copyToClipboard } from "@/lib/clipboard";
-import { buildDataGridCopyUpdateStatements } from "@/lib/dataGridSql";
+import { buildDataGridCopyInsertStatement, buildDataGridCopyUpdateStatements } from "@/lib/dataGridSql";
import type { DatabaseType } from "@/types/database";
interface RowItem {
@@ -42,8 +42,6 @@ export interface UseDataGridExportOptions {
| Ref<{ rowId: number; rowIndex: number; col: number } | null>
| ComputedRef<{ rowId: number; rowIndex: number; col: number } | null>;
getRowItem: (rowId: number) => RowItem | undefined;
- quoteIdent: (name: string) => string;
- escapeVal: (value: CellValue) => string;
selectedRowIds: Ref> | ComputedRef>;
hasRowSelection: ComputedRef;
}
@@ -65,8 +63,6 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
selectedRange,
contextCell,
getRowItem,
- quoteIdent,
- escapeVal,
selectedRowIds,
hasRowSelection,
} = options;
@@ -168,32 +164,29 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
await copyText(JSON.stringify(obj, null, 2));
}
+ function insertEligibleRows(): RowItem[] {
+ return targetedRows();
+ }
+
+ async function copyRowAsInsertStatement(excludePrimaryKeys: boolean) {
+ const statement = buildDataGridCopyInsertStatement({
+ databaseType: databaseType.value,
+ tableMeta: tableMeta.value,
+ columns: columns.value,
+ sourceColumns: sourceColumns.value,
+ rows: insertEligibleRows().map((item) => item.data),
+ excludePrimaryKeys,
+ });
+ if (!statement) return;
+ await copyText(statement);
+ }
+
async function copyRowAsInsert() {
- const table = tableMeta.value
- ? (tableMeta.value.schema ? `${quoteIdent(tableMeta.value.schema)}.` : "") + quoteIdent(tableMeta.value.tableName)
- : "table_name";
- const cols = columns.value.map((c) => quoteIdent(c)).join(", ");
+ await copyRowAsInsertStatement(false);
+ }
- if (hasRowSelection.value && selectedRowIds.value.size > 0) {
- const items = displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
- const valueRows = items.map((item) => `(${item.data.map((v) => escapeVal(v)).join(", ")})`);
- await copyText(`INSERT INTO ${table} (${cols}) VALUES\n${valueRows.join(",\n")};`);
- return;
- }
-
- const range = selectedRange.value;
- if (range && range.startRow !== range.endRow) {
- const items = displayItems.value.slice(range.startRow, range.endRow + 1);
- const valueRows = items.map((item) => `(${item.data.map((v) => escapeVal(v)).join(", ")})`);
- await copyText(`INSERT INTO ${table} (${cols}) VALUES\n${valueRows.join(",\n")};`);
- return;
- }
-
- if (!contextCell.value) return;
- const item = getRowItem(contextCell.value.rowId);
- if (!item) return;
- const vals = item.data.map((v) => escapeVal(v)).join(", ");
- await copyText(`INSERT INTO ${table} (${cols}) VALUES (${vals});`);
+ async function copyRowAsInsertWithoutPrimaryKeys() {
+ await copyRowAsInsertStatement(true);
}
async function copyRowAsUpdate() {
@@ -224,6 +217,20 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
);
});
+ const canCopyRowAsInsertWithoutPrimaryKeys = computed(() => {
+ if (!tableMeta.value?.primaryKeys.length) return false;
+ const rows = insertEligibleRows();
+ if (!rows.length) return false;
+ return !!buildDataGridCopyInsertStatement({
+ databaseType: databaseType.value,
+ tableMeta: tableMeta.value,
+ columns: columns.value,
+ sourceColumns: sourceColumns.value,
+ rows: [rows[0].data],
+ excludePrimaryKeys: true,
+ });
+ });
+
async function copyAll() {
const header = columns.value.join("\t");
const body = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)).join("\t")).join("\n");
@@ -370,7 +377,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
copyCell,
copyRow,
copyRowAsInsert,
+ copyRowAsInsertWithoutPrimaryKeys,
copyRowAsUpdate,
+ canCopyRowAsInsertWithoutPrimaryKeys,
canCopyRowAsUpdate,
copyAll,
copySelectionTsv,
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 905d04e4a6..83384be924 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -255,6 +255,7 @@ export default {
copyCell: "Copy Cell",
copyRow: "Copy Row (JSON)",
copyRowInsert: "Copy as INSERT",
+ copyRowInsertWithoutPrimaryKeys: "Copy as INSERT without Primary Keys",
copyRowUpdate: "Copy as UPDATE",
copyAll: "Copy All (TSV)",
selection: "Selection",
@@ -351,6 +352,7 @@ export default {
restoreRows: "Restore {count} Rows",
copyRows: "Copy {count} Rows (JSON)",
copyRowsInsert: "Copy {count} Rows as INSERT",
+ copyRowsInsertWithoutPrimaryKeys: "Copy {count} Rows as INSERT without Primary Keys",
copyRowsUpdate: "Copy {count} Rows as UPDATE",
selectedRows: "{count} rows selected",
restoreRow: "Restore Row",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index cf3aa3ebd1..a7f9786852 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -250,6 +250,7 @@ export default {
copyCell: "Copiar celda",
copyRow: "Copiar fila (JSON)",
copyRowInsert: "Copiar como INSERT",
+ copyRowInsertWithoutPrimaryKeys: "Copiar como INSERT sin claves primarias",
copyRowUpdate: "Copiar como UPDATE",
copyAll: "Copiar todo (TSV)",
selection: "Selección",
@@ -319,6 +320,7 @@ export default {
restoreRows: "Restaurar {count} filas",
copyRows: "Copiar {count} filas (JSON)",
copyRowsInsert: "Copiar {count} filas como INSERT",
+ copyRowsInsertWithoutPrimaryKeys: "Copiar {count} filas como INSERT sin claves primarias",
copyRowsUpdate: "Copiar {count} filas como UPDATE",
selectedRows: "{count} filas seleccionadas",
restoreRow: "Restaurar fila",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 86fddb0708..704d7dd8f8 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -253,6 +253,7 @@ export default {
copyCell: "复制单元格",
copyRow: "复制行 (JSON)",
copyRowInsert: "复制为 INSERT 语句",
+ copyRowInsertWithoutPrimaryKeys: "复制为 INSERT 语句(不含主键)",
copyRowUpdate: "复制为 UPDATE 语句",
copyAll: "复制全部 (TSV)",
selection: "选区",
@@ -348,6 +349,7 @@ export default {
restoreRows: "恢复 {count} 行",
copyRows: "复制 {count} 行 (JSON)",
copyRowsInsert: "复制 {count} 行为 INSERT",
+ copyRowsInsertWithoutPrimaryKeys: "复制 {count} 行为 INSERT(不含主键)",
copyRowsUpdate: "复制 {count} 行为 UPDATE",
selectedRows: "已选 {count} 行",
restoreRow: "恢复行",
diff --git a/apps/desktop/src/lib/dataGridSql.ts b/apps/desktop/src/lib/dataGridSql.ts
index 7709953d8d..ab8a22e9a3 100644
--- a/apps/desktop/src/lib/dataGridSql.ts
+++ b/apps/desktop/src/lib/dataGridSql.ts
@@ -45,6 +45,15 @@ export interface DataGridCopyUpdateStatementOptions {
rows: GridCellValue[][];
}
+export interface DataGridCopyInsertStatementOptions {
+ databaseType?: DatabaseType;
+ tableMeta?: DataGridTableMeta;
+ columns: string[];
+ sourceColumns?: Array;
+ rows: GridCellValue[][];
+ excludePrimaryKeys?: boolean;
+}
+
export interface DataGridSaveValidationOptions {
databaseType?: DatabaseType;
tableMeta?: DataGridTableMeta;
@@ -249,6 +258,43 @@ export function buildDataGridCopyUpdateStatements(options: DataGridCopyUpdateSta
return statements;
}
+export function buildDataGridCopyInsertStatement(options: DataGridCopyInsertStatementOptions): string | undefined {
+ const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
+ const columnInfo = columnInfoByName(options.tableMeta?.columns);
+ const primaryKeySet = new Set(
+ (options.tableMeta?.primaryKeys ?? []).map((primaryKey) => normalizeColumnName(primaryKey)),
+ );
+ const insertableColumns = saveColumns
+ .map((column, index) => ({ column, index }))
+ .filter((entry): entry is { column: string; index: number } => !!entry.column)
+ .filter((entry) => !isOracleRowId(options.databaseType, entry.column));
+ const insertColumns = insertableColumns.filter(
+ (entry) => !options.excludePrimaryKeys || !primaryKeySet.has(normalizeColumnName(entry.column)),
+ );
+
+ if (options.excludePrimaryKeys && insertColumns.length === insertableColumns.length) return undefined;
+ if (insertColumns.length === 0 || options.rows.length === 0) return undefined;
+
+ const table = options.tableMeta
+ ? qualifiedTableName({
+ databaseType: options.databaseType,
+ schema: options.tableMeta.schema,
+ tableName: options.tableMeta.tableName,
+ })
+ : "table_name";
+ const columns = insertColumns.map((entry) => quoteIdent(options.databaseType, entry.column)).join(", ");
+ const valueRows = options.rows.map(
+ (row) =>
+ `(${insertColumns
+ .map(({ column, index }) =>
+ formatGridSqlLiteral(row[index], options.databaseType, columnInfo.get(normalizeColumnName(column))),
+ )
+ .join(", ")})`,
+ );
+
+ return `INSERT INTO ${table} (${columns}) VALUES${valueRows.length === 1 ? " " : "\n"}${valueRows.join(",\n")};`;
+}
+
function buildTdengineDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] {
const saveColumns = effectiveColumns(options.sourceColumns, options.columns);
const statements: string[] = [];
diff --git a/packages/app-tests/dataGridSql.test.ts b/packages/app-tests/dataGridSql.test.ts
index 7b5afd470c..37a140e4b0 100644
--- a/packages/app-tests/dataGridSql.test.ts
+++ b/packages/app-tests/dataGridSql.test.ts
@@ -1,6 +1,7 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import {
+ buildDataGridCopyInsertStatement,
buildDataGridCopyUpdateStatements,
buildDataGridRollbackStatements,
buildDataGridSaveStatements,
@@ -56,6 +57,58 @@ test("builds copy-as-update statements using primary keys and non-primary-key co
assert.deepEqual(statements, [`UPDATE "public"."users" SET "name" = 'Ada', "status" = 'active' WHERE "id" = 1;`]);
});
+test("builds copy-as-insert statement excluding primary key columns", () => {
+ const statement = buildDataGridCopyInsertStatement({
+ databaseType: "mysql",
+ tableMeta: {
+ tableName: "users",
+ primaryKeys: ["id"],
+ },
+ columns: ["id", "login_name", "display_name"],
+ rows: [
+ [1, "ada", "Ada"],
+ [2, "linus", "Linus"],
+ ],
+ excludePrimaryKeys: true,
+ });
+
+ assert.equal(
+ statement,
+ "INSERT INTO `users` (`login_name`, `display_name`) VALUES\n('ada', 'Ada'),\n('linus', 'Linus');",
+ );
+});
+
+test("copy-as-insert excludes primary keys using source column names", () => {
+ const statement = buildDataGridCopyInsertStatement({
+ databaseType: "mysql",
+ tableMeta: {
+ tableName: "users",
+ primaryKeys: ["user_id"],
+ },
+ columns: ["id", "name"],
+ sourceColumns: ["user_id", "name"],
+ rows: [[7, "Ada"]],
+ excludePrimaryKeys: true,
+ });
+
+ assert.equal(statement, "INSERT INTO `users` (`name`) VALUES ('Ada');");
+});
+
+test("copy-as-insert without primary keys is unavailable when no primary key columns are visible", () => {
+ const statement = buildDataGridCopyInsertStatement({
+ databaseType: "postgres",
+ tableMeta: {
+ tableName: "users",
+ primaryKeys: ["id"],
+ },
+ columns: ["name"],
+ rows: [["Ada"]],
+ excludePrimaryKeys: true,
+ });
+
+ assert.equal(statement, undefined);
+});
+
test("skips copy-as-update statements when primary keys are unavailable", () => {
const statements = buildDataGridCopyUpdateStatements({
databaseType: "postgres",