feat(grid): copy insert without primary keys

This commit is contained in:
t8y2 2026-05-22 11:55:08 +08:00
parent c1fc1c9b70
commit 66d180f729
7 changed files with 152 additions and 31 deletions

View File

@ -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({
<ContextMenuItem @click="copyRowAsInsert">
{{ isMultiRow ? t("grid.copyRowsInsert", { count: multiRowCount }) : t("grid.copyRowInsert") }}
</ContextMenuItem>
<ContextMenuItem v-if="canCopyRowAsInsertWithoutPrimaryKeys" @click="copyRowAsInsertWithoutPrimaryKeys">
{{
isMultiRow
? t("grid.copyRowsInsertWithoutPrimaryKeys", { count: multiRowCount })
: t("grid.copyRowInsertWithoutPrimaryKeys")
}}
</ContextMenuItem>
<ContextMenuItem v-if="canCopyRowAsUpdate" @click="copyRowAsUpdate">
{{ isMultiRow ? t("grid.copyRowsUpdate", { count: multiRowCount }) : t("grid.copyRowUpdate") }}
</ContextMenuItem>

View File

@ -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<Set<number>> | ComputedRef<Set<number>>;
hasRowSelection: ComputedRef<boolean>;
}
@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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: "恢复行",

View File

@ -45,6 +45,15 @@ export interface DataGridCopyUpdateStatementOptions {
rows: GridCellValue[][];
}
export interface DataGridCopyInsertStatementOptions {
databaseType?: DatabaseType;
tableMeta?: DataGridTableMeta;
columns: string[];
sourceColumns?: Array<string | undefined>;
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[] = [];

View File

@ -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",