feat(grid): copy insert without primary keys
This commit is contained in:
parent
c1fc1c9b70
commit
66d180f729
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "恢复行",
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue