fix(grid): reuse prepared row copy statements
This commit is contained in:
parent
ae4bbe1564
commit
f7eec00c8e
|
|
@ -6185,9 +6185,8 @@ const {
|
||||||
copyRowAsInsert,
|
copyRowAsInsert,
|
||||||
copyRowAsInsertWithoutPrimaryKeys,
|
copyRowAsInsertWithoutPrimaryKeys,
|
||||||
prefetchRowAsInsertStatement,
|
prefetchRowAsInsertStatement,
|
||||||
canCopyPreparedInsert,
|
canCopyRowAsInsert,
|
||||||
prefetchRowAsUpdateStatement,
|
prefetchRowAsUpdateStatement,
|
||||||
canCopyPreparedUpdate,
|
|
||||||
copyRowAsUpdate,
|
copyRowAsUpdate,
|
||||||
canCopyRowAsInsertWithoutPrimaryKeys,
|
canCopyRowAsInsertWithoutPrimaryKeys,
|
||||||
canCopyRowAsUpdate,
|
canCopyRowAsUpdate,
|
||||||
|
|
@ -8359,33 +8358,33 @@ function copySubmenu(): ContextMenuItem {
|
||||||
items.push({ label: singleRowSelected ? t("grid.copySelectedRowTsvWithHeaders") : t("grid.copySelectedRowsTsvWithHeaders", { count: selectedRowCount.value }), action: copySelectedRowsTsvWithHeaders });
|
items.push({ label: singleRowSelected ? t("grid.copySelectedRowTsvWithHeaders") : t("grid.copySelectedRowsTsvWithHeaders", { count: selectedRowCount.value }), action: copySelectedRowsTsvWithHeaders });
|
||||||
}
|
}
|
||||||
if (isMultiRow.value) {
|
if (isMultiRow.value) {
|
||||||
items.push({ label: labels.insertMerged, action: () => copyRowAsInsert("merged"), disabled: !canCopyPreparedInsert(false, "merged") });
|
items.push({ label: labels.insertMerged, action: () => copyRowAsInsert("merged"), disabled: !canCopyRowAsInsert.value });
|
||||||
items.push({ label: labels.insertRowByRow, action: () => copyRowAsInsert("row-by-row"), disabled: !canCopyPreparedInsert(false, "row-by-row") });
|
items.push({ label: labels.insertRowByRow, action: () => copyRowAsInsert("row-by-row"), disabled: !canCopyRowAsInsert.value });
|
||||||
} else {
|
} else {
|
||||||
items.push({ label: labels.insert, action: () => copyRowAsInsert(), disabled: !canCopyPreparedInsert(false) });
|
items.push({ label: labels.insert, action: () => copyRowAsInsert(), disabled: !canCopyRowAsInsert.value });
|
||||||
}
|
}
|
||||||
if (canCopyRowAsInsertWithoutPrimaryKeys.value) {
|
if (canCopyRowAsInsertWithoutPrimaryKeys.value) {
|
||||||
if (isMultiRow.value) {
|
if (isMultiRow.value) {
|
||||||
items.push({
|
items.push({
|
||||||
label: labels.insertNoPkMerged,
|
label: labels.insertNoPkMerged,
|
||||||
action: () => copyRowAsInsertWithoutPrimaryKeys("merged"),
|
action: () => copyRowAsInsertWithoutPrimaryKeys("merged"),
|
||||||
disabled: !canCopyPreparedInsert(true, "merged"),
|
disabled: !canCopyRowAsInsertWithoutPrimaryKeys.value,
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
label: labels.insertNoPkRowByRow,
|
label: labels.insertNoPkRowByRow,
|
||||||
action: () => copyRowAsInsertWithoutPrimaryKeys("row-by-row"),
|
action: () => copyRowAsInsertWithoutPrimaryKeys("row-by-row"),
|
||||||
disabled: !canCopyPreparedInsert(true, "row-by-row"),
|
disabled: !canCopyRowAsInsertWithoutPrimaryKeys.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
label: labels.insertNoPk,
|
label: labels.insertNoPk,
|
||||||
action: () => copyRowAsInsertWithoutPrimaryKeys(),
|
action: () => copyRowAsInsertWithoutPrimaryKeys(),
|
||||||
disabled: !canCopyPreparedInsert(true),
|
disabled: !canCopyRowAsInsertWithoutPrimaryKeys.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canCopyRowAsUpdate.value) {
|
if (canCopyRowAsUpdate.value) {
|
||||||
items.push({ label: labels.update, action: copyRowAsUpdate, disabled: !canCopyPreparedUpdate() });
|
items.push({ label: labels.update, action: copyRowAsUpdate });
|
||||||
}
|
}
|
||||||
items.push({ label: t("grid.copyAll"), action: copyAll });
|
items.push({ label: t("grid.copyAll"), action: copyAll });
|
||||||
items.push({ label: t("grid.copyColumnNames"), action: copyColumnNames });
|
items.push({ label: t("grid.copyColumnNames"), action: copyColumnNames });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { useDataGridExport, type UseDataGridExportOptions } from "@/composables/useDataGridExport";
|
||||||
|
import { buildDataGridCopyInsertStatement, buildDataGridCopyUpdateStatements } from "@/lib/dataGrid/dataGridSql";
|
||||||
|
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||||
|
import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
|
||||||
|
|
||||||
|
const toast = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("vue-i18n", () => ({
|
||||||
|
useI18n: () => ({ t: (key: string, params?: { message?: string }) => (params?.message ? `${key}: ${params.message}` : key) }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/composables/useToast", () => ({
|
||||||
|
useToast: () => ({ toast }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/common/clipboard", () => ({
|
||||||
|
copyToClipboard: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/dataGrid/dataGridSql", async (importOriginal) => {
|
||||||
|
const original = await importOriginal<typeof import("@/lib/dataGrid/dataGridSql")>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
buildDataGridCopyInsertStatement: vi.fn(),
|
||||||
|
buildDataGridCopyUpdateStatements: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Deferred<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T) => void;
|
||||||
|
reject: (error: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred<T>(): Deferred<T> {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (error: unknown) => void;
|
||||||
|
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||||
|
resolve = resolvePromise;
|
||||||
|
reject = rejectPromise;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
}
|
||||||
|
|
||||||
|
function row(data: unknown[]) {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
data,
|
||||||
|
isNew: false,
|
||||||
|
isDeleted: false,
|
||||||
|
isDirtyCol: data.map(() => false),
|
||||||
|
status: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExportState(tableMeta: DataGridTableMeta, columns = tableMeta.columns?.map((column) => column.name) ?? ["id", "name"]) {
|
||||||
|
const item = row(columns.map((column, index) => (column === "id" ? 1 : `value-${index}`)));
|
||||||
|
const options: UseDataGridExportOptions = {
|
||||||
|
columns: computed(() => columns),
|
||||||
|
displayItems: computed(() => [item]),
|
||||||
|
sql: computed(() => undefined),
|
||||||
|
tableMeta: computed(() => tableMeta),
|
||||||
|
databaseType: computed(() => "mysql"),
|
||||||
|
connectionId: computed(() => "connection-1"),
|
||||||
|
database: computed(() => "dbx"),
|
||||||
|
context: computed(() => "table-data"),
|
||||||
|
sourceColumns: computed(() => columns),
|
||||||
|
columnTypes: computed(() => columns.map(() => "varchar")),
|
||||||
|
whereInput: computed(() => undefined),
|
||||||
|
orderBy: computed(() => undefined),
|
||||||
|
exportBatchSize: computed(() => 1000),
|
||||||
|
hasCellSelection: computed(() => false),
|
||||||
|
selectedCells: computed(() => ({ columns: [], rows: [] })),
|
||||||
|
selectedRange: computed(() => null),
|
||||||
|
contextCell: ref({ rowId: item.id, rowIndex: 0, col: -1 }),
|
||||||
|
getRowItem: (rowId) => (rowId === item.id ? item : undefined),
|
||||||
|
selectedRowIds: ref(new Set<number>()),
|
||||||
|
hasRowSelection: computed(() => false),
|
||||||
|
};
|
||||||
|
return useDataGridExport(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableTable: DataGridTableMeta = {
|
||||||
|
tableName: "users",
|
||||||
|
primaryKeys: ["id"],
|
||||||
|
columns: [
|
||||||
|
{ name: "id", data_type: "int", is_nullable: false, is_primary_key: true },
|
||||||
|
{ name: "name", data_type: "varchar", is_nullable: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("useDataGridExport prepared row statements", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reuses an in-flight INSERT prefetch when the copy action runs", async () => {
|
||||||
|
const pending = deferred<string | undefined>();
|
||||||
|
vi.mocked(buildDataGridCopyInsertStatement).mockReturnValueOnce(pending.promise);
|
||||||
|
const state = createExportState(editableTable);
|
||||||
|
|
||||||
|
const prefetch = state.prefetchRowAsInsertStatement(false);
|
||||||
|
const copy = state.copyRowAsInsert();
|
||||||
|
await vi.waitFor(() => expect(buildDataGridCopyInsertStatement).toHaveBeenCalledTimes(1));
|
||||||
|
pending.resolve("INSERT INTO users VALUES (1, 'Alice');");
|
||||||
|
|
||||||
|
await Promise.all([prefetch, copy]);
|
||||||
|
expect(copyToClipboard).toHaveBeenCalledWith("INSERT INTO users VALUES (1, 'Alice');");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reuses an in-flight UPDATE prefetch on the first copy action", async () => {
|
||||||
|
const pending = deferred<string[]>();
|
||||||
|
vi.mocked(buildDataGridCopyUpdateStatements).mockReturnValueOnce(pending.promise);
|
||||||
|
const state = createExportState(editableTable);
|
||||||
|
|
||||||
|
const prefetch = state.prefetchRowAsUpdateStatement();
|
||||||
|
const copy = state.copyRowAsUpdate();
|
||||||
|
await vi.waitFor(() => expect(buildDataGridCopyUpdateStatements).toHaveBeenCalledTimes(1));
|
||||||
|
pending.resolve(["UPDATE users SET name = 'Alice' WHERE id = 1;"]);
|
||||||
|
|
||||||
|
await Promise.all([prefetch, copy]);
|
||||||
|
expect(copyToClipboard).toHaveBeenCalledWith("UPDATE users SET name = 'Alice' WHERE id = 1;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["GENERATED ALWAYS AS (1)", "IDENTITY(1, 1)"])("disables copy-as-insert when every result column is non-insertable (%s)", (extra) => {
|
||||||
|
const state = createExportState(
|
||||||
|
{
|
||||||
|
tableName: "generated_values",
|
||||||
|
primaryKeys: [],
|
||||||
|
columns: [{ name: "computed_value", data_type: "int", is_nullable: true, extra }],
|
||||||
|
},
|
||||||
|
["computed_value"],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(state.canCopyRowAsInsert.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a shared builder failure when the user invokes copy", async () => {
|
||||||
|
const pending = deferred<string | undefined>();
|
||||||
|
vi.mocked(buildDataGridCopyInsertStatement).mockReturnValueOnce(pending.promise);
|
||||||
|
const state = createExportState(editableTable);
|
||||||
|
|
||||||
|
const prefetch = state.prefetchRowAsInsertStatement(false);
|
||||||
|
const copy = state.copyRowAsInsert();
|
||||||
|
await vi.waitFor(() => expect(buildDataGridCopyInsertStatement).toHaveBeenCalledTimes(1));
|
||||||
|
pending.reject(new Error("builder unavailable"));
|
||||||
|
|
||||||
|
await Promise.all([prefetch, copy]);
|
||||||
|
expect(toast).toHaveBeenCalledWith("grid.copyFailed: builder unavailable", 5000);
|
||||||
|
expect(copyToClipboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports an UPDATE builder failure from the first copy action", async () => {
|
||||||
|
const pending = deferred<string[]>();
|
||||||
|
vi.mocked(buildDataGridCopyUpdateStatements).mockReturnValueOnce(pending.promise);
|
||||||
|
const state = createExportState(editableTable);
|
||||||
|
|
||||||
|
const prefetch = state.prefetchRowAsUpdateStatement();
|
||||||
|
const copy = state.copyRowAsUpdate();
|
||||||
|
await vi.waitFor(() => expect(buildDataGridCopyUpdateStatements).toHaveBeenCalledTimes(1));
|
||||||
|
pending.reject(new Error("update builder unavailable"));
|
||||||
|
|
||||||
|
await Promise.all([prefetch, copy]);
|
||||||
|
expect(toast).toHaveBeenCalledWith("grid.copyFailed: update builder unavailable", 5000);
|
||||||
|
expect(copyToClipboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,6 +15,7 @@ import { expandNestedJsonStringsForCopy } from "@/lib/common/jsonCopyValue";
|
||||||
import { buildMongoCopyInsertDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues";
|
import { buildMongoCopyInsertDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues";
|
||||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
import type { DatabaseType, QueryResult } from "@/types/database";
|
||||||
import type { QueryResultExportRequest } from "@/lib/backend/api";
|
import type { QueryResultExportRequest } from "@/lib/backend/api";
|
||||||
|
import { DBX_ROWID_COLUMN } from "@/lib/table/tableEditing";
|
||||||
|
|
||||||
interface RowItem {
|
interface RowItem {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -90,6 +91,7 @@ interface CopyStatementCache {
|
||||||
text: string;
|
text: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
promise?: Promise<string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDataGridExport(options: UseDataGridExportOptions) {
|
export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
|
|
@ -276,6 +278,14 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prefetchRowAsInsertStatement(excludePrimaryKeys: boolean, insertMode: DataGridCopyInsertMode = "merged") {
|
async function prefetchRowAsInsertStatement(excludePrimaryKeys: boolean, insertMode: DataGridCopyInsertMode = "merged") {
|
||||||
|
try {
|
||||||
|
await prepareRowAsInsertStatement(excludePrimaryKeys, insertMode);
|
||||||
|
} catch {
|
||||||
|
// Prefetch failures are reported only if the user invokes the copy action.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareRowAsInsertStatement(excludePrimaryKeys: boolean, insertMode: DataGridCopyInsertMode = "merged"): Promise<string | undefined> {
|
||||||
const rows = insertEligibleRows();
|
const rows = insertEligibleRows();
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
||||||
|
|
@ -288,16 +298,10 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
}
|
}
|
||||||
const key = insertCopyKey(excludePrimaryKeys, insertMode);
|
const key = insertCopyKey(excludePrimaryKeys, insertMode);
|
||||||
const current = insertCopyCache(excludePrimaryKeys, insertMode);
|
const current = insertCopyCache(excludePrimaryKeys, insertMode);
|
||||||
if ((current.loading || current.ready) && current.key === key) return;
|
if (current.ready && current.key === key) return current.text;
|
||||||
|
if (current.loading && current.key === key && current.promise) return current.promise;
|
||||||
|
|
||||||
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
const promise = Promise.resolve().then(async () => {
|
||||||
key,
|
|
||||||
text: "",
|
|
||||||
loading: true,
|
|
||||||
ready: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const statement =
|
const statement =
|
||||||
databaseType.value === "mongodb"
|
databaseType.value === "mongodb"
|
||||||
? buildMongoCopyInsertStatement({
|
? buildMongoCopyInsertStatement({
|
||||||
|
|
@ -319,22 +323,37 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
insertMode,
|
insertMode,
|
||||||
});
|
});
|
||||||
const latest = insertCopyCache(excludePrimaryKeys, insertMode);
|
const latest = insertCopyCache(excludePrimaryKeys, insertMode);
|
||||||
if (latest.key !== key) return;
|
if (latest.key !== key || latest.promise !== promise) return undefined;
|
||||||
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
||||||
key,
|
key,
|
||||||
text: statement ?? "",
|
text: statement ?? "",
|
||||||
loading: false,
|
loading: false,
|
||||||
ready: !!statement,
|
ready: !!statement,
|
||||||
});
|
});
|
||||||
} catch {
|
return statement;
|
||||||
|
});
|
||||||
|
|
||||||
|
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
||||||
|
key,
|
||||||
|
text: "",
|
||||||
|
loading: true,
|
||||||
|
ready: false,
|
||||||
|
promise,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (error) {
|
||||||
const latest = insertCopyCache(excludePrimaryKeys, insertMode);
|
const latest = insertCopyCache(excludePrimaryKeys, insertMode);
|
||||||
if (latest.key !== key) return;
|
if (latest.key === key && latest.promise === promise) {
|
||||||
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
setInsertCopyCache(excludePrimaryKeys, insertMode, {
|
||||||
key,
|
key,
|
||||||
text: "",
|
text: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,14 +362,29 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
return cache.ready && cache.key === insertCopyKey(excludePrimaryKeys, insertMode);
|
return cache.ready && cache.key === insertCopyKey(excludePrimaryKeys, insertMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyPreparedRowAsInsert(excludePrimaryKeys: boolean, insertMode: DataGridCopyInsertMode = "merged"): boolean {
|
async function copyPreparedRowAsInsert(excludePrimaryKeys: boolean, insertMode: DataGridCopyInsertMode = "merged"): Promise<boolean> {
|
||||||
if (!canCopyPreparedInsert(excludePrimaryKeys, insertMode)) return false;
|
try {
|
||||||
void copyText(insertCopyCache(excludePrimaryKeys, insertMode).text);
|
const statement = await prepareRowAsInsertStatement(excludePrimaryKeys, insertMode);
|
||||||
return true;
|
if (!statement) return false;
|
||||||
|
await copyText(statement);
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast(t("grid.copyFailed", { message: error?.message || String(error) }), 5000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prefetchRowAsUpdateStatement() {
|
async function prefetchRowAsUpdateStatement() {
|
||||||
if (!tableMeta.value?.primaryKeys.length) {
|
try {
|
||||||
|
await prepareRowAsUpdateStatement();
|
||||||
|
} catch {
|
||||||
|
// Prefetch failures are reported only if the user invokes the copy action.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareRowAsUpdateStatement(): Promise<string | undefined> {
|
||||||
|
const currentTableMeta = tableMeta.value;
|
||||||
|
if (!currentTableMeta?.primaryKeys.length) {
|
||||||
setUpdateCopyCache({
|
setUpdateCopyCache({
|
||||||
key: "",
|
key: "",
|
||||||
text: "",
|
text: "",
|
||||||
|
|
@ -371,25 +405,19 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
}
|
}
|
||||||
const key = updateCopyKey();
|
const key = updateCopyKey();
|
||||||
const current = copyRowUpdateCache.value;
|
const current = copyRowUpdateCache.value;
|
||||||
if ((current.loading || current.ready) && current.key === key) return;
|
if (current.ready && current.key === key) return current.text;
|
||||||
|
if (current.loading && current.key === key && current.promise) return current.promise;
|
||||||
|
|
||||||
setUpdateCopyCache({
|
const promise = Promise.resolve().then(async () => {
|
||||||
key,
|
|
||||||
text: "",
|
|
||||||
loading: true,
|
|
||||||
ready: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const statements = await buildDataGridCopyUpdateStatements({
|
const statements = await buildDataGridCopyUpdateStatements({
|
||||||
databaseType: databaseType.value,
|
databaseType: databaseType.value,
|
||||||
tableMeta: tableMeta.value,
|
tableMeta: currentTableMeta,
|
||||||
columns: columns.value,
|
columns: columns.value,
|
||||||
sourceColumns: sourceColumns.value,
|
sourceColumns: sourceColumns.value,
|
||||||
rows: rows.map((item) => item.data),
|
rows: rows.map((item) => item.data),
|
||||||
});
|
});
|
||||||
const latest = copyRowUpdateCache.value;
|
const latest = copyRowUpdateCache.value;
|
||||||
if (latest.key !== key) return;
|
if (latest.key !== key || latest.promise !== promise) return undefined;
|
||||||
const text = statements.join("\n");
|
const text = statements.join("\n");
|
||||||
setUpdateCopyCache({
|
setUpdateCopyCache({
|
||||||
key,
|
key,
|
||||||
|
|
@ -397,15 +425,30 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
loading: false,
|
loading: false,
|
||||||
ready: statements.length > 0,
|
ready: statements.length > 0,
|
||||||
});
|
});
|
||||||
} catch {
|
return text || undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
setUpdateCopyCache({
|
||||||
|
key,
|
||||||
|
text: "",
|
||||||
|
loading: true,
|
||||||
|
ready: false,
|
||||||
|
promise,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (error) {
|
||||||
const latest = copyRowUpdateCache.value;
|
const latest = copyRowUpdateCache.value;
|
||||||
if (latest.key !== key) return;
|
if (latest.key === key && latest.promise === promise) {
|
||||||
setUpdateCopyCache({
|
setUpdateCopyCache({
|
||||||
key,
|
key,
|
||||||
text: "",
|
text: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,10 +457,16 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
return cache.ready && cache.key === updateCopyKey();
|
return cache.ready && cache.key === updateCopyKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyPreparedRowAsUpdate(): boolean {
|
async function copyPreparedRowAsUpdate(): Promise<boolean> {
|
||||||
if (!canCopyPreparedUpdate()) return false;
|
try {
|
||||||
void copyText(copyRowUpdateCache.value.text);
|
const statement = await prepareRowAsUpdateStatement();
|
||||||
return true;
|
if (!statement) return false;
|
||||||
|
await copyText(statement);
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast(t("grid.copyFailed", { message: error?.message || String(error) }), 5000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Selection copy functions ---
|
// --- Selection copy functions ---
|
||||||
|
|
@ -512,15 +561,15 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyRowAsInsert(insertMode: DataGridCopyInsertMode = "merged") {
|
async function copyRowAsInsert(insertMode: DataGridCopyInsertMode = "merged") {
|
||||||
copyPreparedRowAsInsert(false, insertMode);
|
await copyPreparedRowAsInsert(false, insertMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyRowAsInsertWithoutPrimaryKeys(insertMode: DataGridCopyInsertMode = "merged") {
|
async function copyRowAsInsertWithoutPrimaryKeys(insertMode: DataGridCopyInsertMode = "merged") {
|
||||||
copyPreparedRowAsInsert(true, insertMode);
|
await copyPreparedRowAsInsert(true, insertMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyRowAsUpdate() {
|
async function copyRowAsUpdate() {
|
||||||
copyPreparedRowAsUpdate();
|
await copyPreparedRowAsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCopyRowAsUpdate = computed(() => {
|
const canCopyRowAsUpdate = computed(() => {
|
||||||
|
|
@ -535,14 +584,19 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
return saveColumns.some((column) => column && !primaryKeySet.has(normalizeColumnName(column)));
|
return saveColumns.some((column) => column && !primaryKeySet.has(normalizeColumnName(column)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function insertableCopyColumnCount(excludePrimaryKeys: boolean): number {
|
||||||
|
const primaryKeySet = new Set((tableMeta.value?.primaryKeys ?? []).map(normalizeColumnName));
|
||||||
|
return effectiveColumns(sourceColumns.value, columns.value).filter((column): column is string => !!column && !isCopyInsertOmittedColumn(databaseType.value, column, tableMeta.value) && (!excludePrimaryKeys || !primaryKeySet.has(normalizeColumnName(column)))).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canCopyRowAsInsert = computed(() => insertEligibleRows().length > 0 && insertableCopyColumnCount(false) > 0);
|
||||||
|
|
||||||
const canCopyRowAsInsertWithoutPrimaryKeys = computed(() => {
|
const canCopyRowAsInsertWithoutPrimaryKeys = computed(() => {
|
||||||
if (!tableMeta.value?.primaryKeys.length) return false;
|
if (!tableMeta.value?.primaryKeys.length) return false;
|
||||||
const rows = insertEligibleRows();
|
const rows = insertEligibleRows();
|
||||||
if (!rows.length) return false;
|
if (!rows.length) return false;
|
||||||
const saveColumns = effectiveColumns(sourceColumns.value, columns.value);
|
const insertableCount = insertableCopyColumnCount(false);
|
||||||
const primaryKeySet = new Set(tableMeta.value.primaryKeys.map(normalizeColumnName));
|
const insertColumnsCount = insertableCopyColumnCount(true);
|
||||||
const insertableCount = saveColumns.filter(Boolean).length;
|
|
||||||
const insertColumnsCount = saveColumns.filter((column) => column && !primaryKeySet.has(normalizeColumnName(column))).length;
|
|
||||||
return insertColumnsCount > 0 && insertColumnsCount < insertableCount;
|
return insertColumnsCount > 0 && insertColumnsCount < insertableCount;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1108,6 +1162,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||||
prefetchRowAsInsertStatement,
|
prefetchRowAsInsertStatement,
|
||||||
canCopyPreparedInsert,
|
canCopyPreparedInsert,
|
||||||
copyPreparedRowAsInsert,
|
copyPreparedRowAsInsert,
|
||||||
|
canCopyRowAsInsert,
|
||||||
prefetchRowAsUpdateStatement,
|
prefetchRowAsUpdateStatement,
|
||||||
canCopyPreparedUpdate,
|
canCopyPreparedUpdate,
|
||||||
copyPreparedRowAsUpdate,
|
copyPreparedRowAsUpdate,
|
||||||
|
|
@ -1213,6 +1268,15 @@ function effectiveColumns(sourceColumns: Array<string | undefined> | undefined,
|
||||||
return sourceColumns;
|
return sourceColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCopyInsertOmittedColumn(databaseType: DatabaseType | undefined, column: string, tableMeta: DataGridTableMeta | undefined): boolean {
|
||||||
|
if (databaseType === "oracle" && column.toUpperCase() === DBX_ROWID_COLUMN) return true;
|
||||||
|
const columnInfo = tableMeta?.columns?.find((item) => normalizeColumnName(item.name) === normalizeColumnName(column));
|
||||||
|
const normalizedType = columnInfo?.data_type.trim().replace(/^"|"$/g, "").toLowerCase();
|
||||||
|
if (databaseType === "postgres" && (normalizedType === "tsvector" || normalizedType?.endsWith(".tsvector"))) return true;
|
||||||
|
const extra = columnInfo?.extra?.toLowerCase() ?? "";
|
||||||
|
return /\b(auto_increment|autoincrement|identity)\b/.test(extra) || (extra.includes("generated always as") && !extra.includes("identity"));
|
||||||
|
}
|
||||||
|
|
||||||
function findColumnIndex(columns: Array<string | undefined>, target: string): number {
|
function findColumnIndex(columns: Array<string | undefined>, target: string): number {
|
||||||
const normalizedTarget = normalizeColumnName(target);
|
const normalizedTarget = normalizeColumnName(target);
|
||||||
return columns.findIndex((column) => (column ? normalizeColumnName(column) : "") === normalizedTarget);
|
return columns.findIndex((column) => (column ? normalizeColumnName(column) : "") === normalizedTarget);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue