feat(export): add SQL INSERT batch or single-row mode
This commit is contained in:
1 parent
5d65be03a3
commit
ab35a5b1da
29 files changed
+563
-23
No files matched your search
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
|
||||
const { t } = useI18n();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const selected = ref<SqlInsertMode>("batch");
|
||||
let outcomeEmitted = false;
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [mode: SqlInsertMode];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
function onConfirm() {
|
||||
outcomeEmitted = true;
|
||||
open.value = false;
|
||||
emit("confirm", selected.value);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
if (outcomeEmitted) return;
|
||||
outcomeEmitted = true;
|
||||
open.value = false;
|
||||
emit("cancel");
|
||||
}
|
||||
|
||||
function onOpenChange(value: boolean) {
|
||||
if (!value) onCancel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open" @update:open="onOpenChange">
|
||||
<DialogContent class="sm:max-w-md" @interact-outside.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("grid.sqlInsertModeTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3 py-2">
|
||||
<p class="text-sm text-muted-foreground">{{ t("grid.sqlInsertModePrompt") }}</p>
|
||||
<label class="flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors hover:bg-accent/50">
|
||||
<input v-model="selected" type="radio" value="batch" class="mt-0.5 h-4 w-4 shrink-0" data-sql-insert-mode="batch" />
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-medium">{{ t("grid.sqlInsertModeBatch") }}</span>
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("grid.sqlInsertModeBatchDescription") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors hover:bg-accent/50">
|
||||
<input v-model="selected" type="radio" value="single" class="mt-0.5 h-4 w-4 shrink-0" data-sql-insert-mode="single" />
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-medium">{{ t("grid.sqlInsertModeSingle") }}</span>
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("grid.sqlInsertModeSingleDescription") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="onCancel">{{ t("common.cancel") }}</Button>
|
||||
<Button data-sql-insert-mode-confirm @click="onConfirm">{{ t("common.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, h, nextTick, type App, defineComponent } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
vi.mock("@/components/ui/dialog", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const passthrough = defineComponent({
|
||||
inheritAttrs: false,
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => h("div", attrs, slots.default?.());
|
||||
},
|
||||
});
|
||||
return { Dialog: passthrough, DialogContent: passthrough, DialogHeader: passthrough, DialogTitle: passthrough, DialogFooter: passthrough };
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/button", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Button: defineComponent({
|
||||
inheritAttrs: false,
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => h("button", attrs, slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import SqlInsertModeDialog from "@/components/export/SqlInsertModeDialog.vue";
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
i18n.global.locale.value = "en";
|
||||
});
|
||||
|
||||
async function mountDialog(onConfirm = () => {}, onCancel = () => {}) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(SqlInsertModeDialog, { open: true, onConfirm, onCancel });
|
||||
},
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
describe("SqlInsertModeDialog", () => {
|
||||
it("defaults to batch mode and emits the selected single-row mode", async () => {
|
||||
const onConfirm = vi.fn();
|
||||
await mountDialog(onConfirm);
|
||||
|
||||
const batch = document.querySelector<HTMLInputElement>('input[data-sql-insert-mode="batch"]');
|
||||
const single = document.querySelector<HTMLInputElement>('input[data-sql-insert-mode="single"]');
|
||||
expect(batch?.checked).toBe(true);
|
||||
expect(single?.checked).toBe(false);
|
||||
|
||||
single?.click();
|
||||
await nextTick();
|
||||
document.querySelector<HTMLButtonElement>("[data-sql-insert-mode-confirm]")?.click();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
expect(onConfirm).toHaveBeenCalledWith("single");
|
||||
});
|
||||
|
||||
it("renders the batch and single-row explanations", async () => {
|
||||
await mountDialog();
|
||||
|
||||
expect(document.body.textContent).toContain("Batch append");
|
||||
expect(document.body.textContent).toContain("One row per statement");
|
||||
expect(document.body.textContent).toContain("Combine multiple rows into each INSERT statement");
|
||||
expect(document.body.textContent).toContain("Write one complete INSERT statement per row");
|
||||
});
|
||||
|
||||
it("emits cancel without selecting a mode", async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
await mountDialog(onConfirm, onCancel);
|
||||
|
||||
const cancel = [...document.querySelectorAll("button")].find((button) => button.textContent?.includes("Cancel"));
|
||||
cancel?.click();
|
||||
|
||||
expect(onCancel).toHaveBeenCalledOnce();
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -91,6 +91,7 @@ import { loadObjectDdl } from "@/lib/metadata/objectDdlCache";
|
||||
import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion";
|
||||
import { createColumnDrafts } from "@/lib/table/tableStructureEditorState";
|
||||
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructureEditorSql";
|
||||
@@ -511,7 +512,15 @@ interface DataGridProps {
|
||||
exportSql?: string;
|
||||
onExecuteSql?: (sql: string) => Promise<void>;
|
||||
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
|
||||
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt" | "sql"; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined> }) => Promise<api.QueryResultExportRequest | undefined>;
|
||||
queryResultExportRequest?: (options: {
|
||||
exportId: string;
|
||||
filePath: string;
|
||||
format: "csv" | "xlsx" | "txt" | "sql";
|
||||
includeSqlSheet?: boolean;
|
||||
exportTableName?: string;
|
||||
exportColumnTypes?: Array<string | null | undefined>;
|
||||
insertMode?: SqlInsertMode;
|
||||
}) => Promise<api.QueryResultExportRequest | undefined>;
|
||||
allExportResults?: Array<{
|
||||
sheetName: string;
|
||||
result: QueryResult;
|
||||
|
||||
@@ -154,6 +154,7 @@ import { chartableColumnIndexes } from "@/lib/dataGrid/chartData";
|
||||
import { elasticsearchJsonResponseForResult } from "@/lib/elasticsearch/elasticsearchJsonResponse";
|
||||
import { elasticsearchProfileBodyForResult, parseElasticsearchProfile } from "@/lib/elasticsearch/elasticsearchProfile";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
import { applyMongoGridChangesToDocument, applyMongoGridChangesToDocumentBaseline, buildMongoUpdateDocument, formatMongoShellLiteral, serializeMongoDocumentId, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues";
|
||||
import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
|
||||
import { isDataGridToolbarCompact, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar";
|
||||
@@ -1889,7 +1890,8 @@ defineExpose({
|
||||
:on-execute-sql="async (sql: string) => emit('executeSql', activeTab.id, sql)"
|
||||
:full-export-result="(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => queryStore.fetchTabResultForExport(activeTab.id, onProgress)"
|
||||
:query-result-export-request="
|
||||
(options: { exportId: string; filePath: string; format: 'csv' | 'xlsx' | 'txt' | 'sql'; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined> }) => queryStore.buildQueryResultExportRequest(activeTab.id, options)
|
||||
(options: { exportId: string; filePath: string; format: 'csv' | 'xlsx' | 'txt' | 'sql'; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined>; insertMode?: SqlInsertMode }) =>
|
||||
queryStore.buildQueryResultExportRequest(activeTab.id, options)
|
||||
"
|
||||
:all-export-results="allResultExportSheets"
|
||||
:export-file-base-name="activeTab.title"
|
||||
|
||||
@@ -92,6 +92,7 @@ import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRe
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
import { showSqlInsertModeDialog, type SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/common/clipboard";
|
||||
import {
|
||||
defaultPasteTableMode,
|
||||
@@ -2097,8 +2098,13 @@ async function exportDataLegacy(row: ObjectBrowserRow, format: "json") {
|
||||
}
|
||||
|
||||
async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql") {
|
||||
if (format === "json") await exportDataLegacy(row, format);
|
||||
else await exportTableData(row, format);
|
||||
if (format === "json") {
|
||||
await exportDataLegacy(row, format);
|
||||
return;
|
||||
}
|
||||
const insertMode = format === "sql" ? await showSqlInsertModeDialog() : undefined;
|
||||
if (format === "sql" && insertMode === null) return;
|
||||
await exportTableData(row, format, undefined, "name", true, insertMode ?? "batch");
|
||||
}
|
||||
|
||||
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<XlsxExportOptions | null> {
|
||||
@@ -2141,7 +2147,7 @@ async function exportDataXlsx(row: ObjectBrowserRow) {
|
||||
await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter);
|
||||
}
|
||||
|
||||
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name", autoFilter = true) {
|
||||
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name", autoFilter = true, insertMode: SqlInsertMode = "batch") {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
|
||||
// Save dialog first
|
||||
@@ -2215,6 +2221,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
tableName: row.name,
|
||||
filePath,
|
||||
format,
|
||||
...(format === "sql" ? { insertMode } : {}),
|
||||
csvQuoteMode: settingsStore.editorSettings.csvQuoteMode,
|
||||
columns,
|
||||
columnComments: format === "xlsx" ? columnComments : undefined,
|
||||
|
||||
@@ -26,6 +26,15 @@ describe("ObjectBrowser XLSX export", () => {
|
||||
expect(exportDataXlsx.indexOf("await showObjectBrowserXlsxHeaderDialog(")).toBeLessThan(exportDataXlsx.indexOf('await exportTableData(row, "xlsx", columnInfos, exportOptions.headerMode, exportOptions.autoFilter)'));
|
||||
});
|
||||
|
||||
it("asks for the SQL INSERT mode before opening the save dialog", () => {
|
||||
const exportData = functionBody("exportData");
|
||||
const exportTableData = functionBody("exportTableData");
|
||||
|
||||
expect(exportData).toContain("await showSqlInsertModeDialog()");
|
||||
expect(exportData.indexOf("await showSqlInsertModeDialog()")).toBeLessThan(exportData.indexOf("await exportTableData("));
|
||||
expect(exportTableData).toContain('...(format === "sql" ? { insertMode } : {})');
|
||||
});
|
||||
|
||||
it("falls back to field-name headers when column metadata is unavailable", () => {
|
||||
const exportDataXlsx = functionBody("exportDataXlsx");
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { clearDataGridClipboardCopy, rememberDataGridClipboardCopy } from "@/lib/dataGrid/dataGridClipboard";
|
||||
import { buildDataGridCopyInsertStatement, type DataGridCopyInsertMode, type DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
|
||||
import { formatSqlInsert, formatTsv } from "@/lib/export/exportFormats";
|
||||
import { showSqlInsertModeDialog, type SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { expandNestedJsonStringsForCopy } from "@/lib/common/jsonCopyValue";
|
||||
@@ -106,7 +107,15 @@ export interface UseDataGridExportOptions {
|
||||
hasRowSelection: ComputedRef<boolean>;
|
||||
resolveSourceValues?: (rowIds: number[], sourceColumnIndexes: number[]) => Promise<Map<number, Map<number, CellValue>>>;
|
||||
fullExportResult?: (onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => Promise<QueryResult | undefined>;
|
||||
queryResultExportRequest?: (options: { exportId: string; filePath: string; format: "csv" | "xlsx" | "txt" | "sql"; includeSqlSheet?: boolean; exportTableName?: string; exportColumnTypes?: Array<string | null | undefined> }) => Promise<QueryResultExportRequest | undefined>;
|
||||
queryResultExportRequest?: (options: {
|
||||
exportId: string;
|
||||
filePath: string;
|
||||
format: "csv" | "xlsx" | "txt" | "sql";
|
||||
includeSqlSheet?: boolean;
|
||||
exportTableName?: string;
|
||||
exportColumnTypes?: Array<string | null | undefined>;
|
||||
insertMode?: SqlInsertMode;
|
||||
}) => Promise<QueryResultExportRequest | undefined>;
|
||||
/**
|
||||
* True when the in-memory result already holds the complete result set —
|
||||
* i.e. the query ran without server-side pagination, was not truncated, and
|
||||
@@ -1104,7 +1113,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
await exportAllResultsXlsxResult(true);
|
||||
}
|
||||
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], headerMode: XlsxHeaderMode = "name", autoFilter = true): Promise<boolean> {
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], headerMode: XlsxHeaderMode = "name", autoFilter = true, insertMode?: SqlInsertMode): Promise<boolean> {
|
||||
const meta = tableMeta.value;
|
||||
// The backend table exporter currently builds two-part table names. External
|
||||
// Doris/StarRocks catalogs need the data-tab paginator's three-part SQL.
|
||||
@@ -1163,6 +1172,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
tableName: meta.tableName,
|
||||
filePath: outputPath,
|
||||
format,
|
||||
...(format === "sql" && insertMode ? { insertMode } : {}),
|
||||
csvQuoteMode: editorSettings.csvQuoteMode,
|
||||
columns: columns.value,
|
||||
columnTypes: columnTypes.value,
|
||||
@@ -1203,7 +1213,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx" | "txt" | "sql", rowIds?: number[], includeSqlSheet = false, headerMode: XlsxHeaderMode = "name", autoFilter = true): Promise<boolean> {
|
||||
async function exportQueryResultViaBackend(format: "csv" | "xlsx" | "txt" | "sql", rowIds?: number[], includeSqlSheet = false, headerMode: XlsxHeaderMode = "name", autoFilter = true, insertMode?: SqlInsertMode): Promise<boolean> {
|
||||
if (rowIds !== undefined || context.value !== "results" || !queryResultExportRequest) {
|
||||
return false;
|
||||
}
|
||||
@@ -1234,6 +1244,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
includeSqlSheet,
|
||||
exportTableName: format === "sql" ? tableMeta.value?.tableName : undefined,
|
||||
exportColumnTypes: format === "sql" ? allColumnTypes.value?.map((type) => type ?? null) : undefined,
|
||||
...(format === "sql" && insertMode ? { insertMode } : {}),
|
||||
});
|
||||
const columnComments = format === "xlsx" ? buildXlsxHeaderOverrides(allColumns.value, allXlsxColumnComments.value, headerMode) : undefined;
|
||||
const request = baseRequest
|
||||
@@ -1316,19 +1327,21 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function exportQueryResultSqlViaBackend(rowIds?: number[]): Promise<boolean> {
|
||||
async function exportQueryResultSqlViaBackend(rowIds: number[] | undefined, insertMode: SqlInsertMode): Promise<boolean> {
|
||||
if (!isTauriRuntime()) return false;
|
||||
return exportQueryResultViaBackend("sql", rowIds);
|
||||
return exportQueryResultViaBackend("sql", rowIds, false, "name", true, insertMode);
|
||||
}
|
||||
|
||||
async function exportSql(rowIds?: number[]) {
|
||||
await runExclusiveExport(async () => {
|
||||
const insertMode = await showSqlInsertModeDialog();
|
||||
if (insertMode === null) return;
|
||||
try {
|
||||
// Step 1: table-data context — existing backend table export
|
||||
if (await exportFullTableDataViaBackend("sql", rowIds)) return;
|
||||
if (await exportFullTableDataViaBackend("sql", rowIds, "name", true, insertMode)) return;
|
||||
|
||||
// Step 2: query-result context — NEW backend streaming with background task
|
||||
if (await exportQueryResultSqlViaBackend(rowIds)) return;
|
||||
if (await exportQueryResultSqlViaBackend(rowIds, insertMode)) return;
|
||||
|
||||
// Step 3: fallback — local export (Web and edge-case scenarios)
|
||||
const result = await resultToExport(rowIds, undefined, true, false);
|
||||
@@ -1343,6 +1356,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
spatialColumns: exportData.spatialColumns,
|
||||
spatialValues: exportData.spatialValues,
|
||||
rows: exportData.rows,
|
||||
insertMode,
|
||||
});
|
||||
await saveTextFile(content, exportFileName(tableMeta.value?.tableName || "export", "sql", { preferFallback: true }), "SQL", "sql");
|
||||
toast(t("grid.exported"));
|
||||
@@ -1354,6 +1368,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
|
||||
async function exportCurrentPageSql() {
|
||||
await runExclusiveExport(async () => {
|
||||
const insertMode = await showSqlInsertModeDialog();
|
||||
if (insertMode === null) return;
|
||||
try {
|
||||
const result = await resultToExport(undefined, undefined, false, false);
|
||||
const exportData = sqlInsertExportData(result);
|
||||
@@ -1367,6 +1383,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
spatialColumns: exportData.spatialColumns,
|
||||
spatialValues: exportData.spatialValues,
|
||||
rows: exportData.rows,
|
||||
insertMode,
|
||||
});
|
||||
await saveTextFile(content, exportFileName("export-page", "sql", { page: true }), "SQL", "sql");
|
||||
toast(t("grid.exported"));
|
||||
|
||||
@@ -1722,6 +1722,12 @@ export default {
|
||||
exportJson: "Export JSON",
|
||||
exportMarkdown: "Export Markdown",
|
||||
exportSql: "Export SQL INSERT",
|
||||
sqlInsertModeTitle: "SQL INSERT Export Mode",
|
||||
sqlInsertModePrompt: "Choose how rows are written to the SQL INSERT file:",
|
||||
sqlInsertModeBatch: "Batch append",
|
||||
sqlInsertModeBatchDescription: "Combine multiple rows into each INSERT statement for smaller files and faster execution.",
|
||||
sqlInsertModeSingle: "One row per statement",
|
||||
sqlInsertModeSingleDescription: "Write one complete INSERT statement per row for easier individual execution and troubleshooting.",
|
||||
exportTxt: "Export TXT",
|
||||
exportSelectedRowsCsv: "Export Selected Rows as CSV",
|
||||
exportSelectedRowsXlsx: "Export Selected Rows as XLSX",
|
||||
|
||||
@@ -1603,6 +1603,12 @@ export default withEnglishFallback({
|
||||
exportJson: "Exportar JSON",
|
||||
exportMarkdown: "Exportar Markdown",
|
||||
exportSql: "Exportar SQL INSERT",
|
||||
sqlInsertModeTitle: "Modo de exportación SQL INSERT",
|
||||
sqlInsertModePrompt: "Elige cómo se escribirán las filas en el archivo SQL INSERT:",
|
||||
sqlInsertModeBatch: "Agregar por lotes",
|
||||
sqlInsertModeBatchDescription: "Combina varias filas en cada instrucción INSERT para obtener archivos más pequeños y una ejecución más rápida.",
|
||||
sqlInsertModeSingle: "Una fila por instrucción",
|
||||
sqlInsertModeSingleDescription: "Escribe una instrucción INSERT completa por fila para facilitar la ejecución individual y la resolución de problemas.",
|
||||
exportTxt: "Exportar TXT",
|
||||
exportSelectedRowsCsv: "Exportar filas seleccionadas como CSV",
|
||||
exportSelectedRowsXlsx: "Exportar filas seleccionadas como XLSX",
|
||||
|
||||
@@ -1601,6 +1601,12 @@ export default withEnglishFallback({
|
||||
exportJson: "Esporta JSON",
|
||||
exportMarkdown: "Esporta Markdown",
|
||||
exportSql: "Esporta SQL INSERT",
|
||||
sqlInsertModeTitle: "Modalità di esportazione SQL INSERT",
|
||||
sqlInsertModePrompt: "Scegli come scrivere le righe nel file SQL INSERT:",
|
||||
sqlInsertModeBatch: "Aggiunta in blocco",
|
||||
sqlInsertModeBatchDescription: "Combina più righe in ogni istruzione INSERT per ottenere file più piccoli e un'esecuzione più rapida.",
|
||||
sqlInsertModeSingle: "Una riga per istruzione",
|
||||
sqlInsertModeSingleDescription: "Scrive un'istruzione INSERT completa per ogni riga per facilitare l'esecuzione singola e la risoluzione dei problemi.",
|
||||
exportTxt: "Esporta TXT",
|
||||
exportSelectedRowsCsv: "Esporta Righe Selezionate come CSV",
|
||||
exportSelectedRowsXlsx: "Esporta Righe Selezionate come XLSX",
|
||||
|
||||
@@ -1615,6 +1615,12 @@ export default withEnglishFallback({
|
||||
exportJson: "JSONエクスポート",
|
||||
exportMarkdown: "Markdownエクスポート",
|
||||
exportSql: "SQL INSERTエクスポート",
|
||||
sqlInsertModeTitle: "SQL INSERT エクスポートモード",
|
||||
sqlInsertModePrompt: "SQL INSERT ファイルへの行の書き出し方法を選択してください:",
|
||||
sqlInsertModeBatch: "一括追加",
|
||||
sqlInsertModeBatchDescription: "複数の行を各 INSERT 文にまとめ、ファイルを小さくして実行を高速化します。",
|
||||
sqlInsertModeSingle: "1 行ずつ独立した文",
|
||||
sqlInsertModeSingleDescription: "行ごとに完全な INSERT 文を書き出し、個別の実行やトラブルシューティングを容易にします。",
|
||||
exportTxt: "TXTエクスポート",
|
||||
exportSelectedRowsCsv: "選択行をCSVでエクスポート",
|
||||
exportSelectedRowsXlsx: "選択行をXLSXでエクスポート",
|
||||
|
||||
@@ -1593,6 +1593,12 @@ export default withEnglishFallback({
|
||||
exportJson: "JSON 내보내기",
|
||||
exportMarkdown: "Markdown 내보내기",
|
||||
exportSql: "SQL INSERT 내보내기",
|
||||
sqlInsertModeTitle: "SQL INSERT 내보내기 모드",
|
||||
sqlInsertModePrompt: "SQL INSERT 파일에 행을 기록할 방식을 선택하세요:",
|
||||
sqlInsertModeBatch: "일괄 추가",
|
||||
sqlInsertModeBatchDescription: "여러 행을 각 INSERT 문에 결합하여 파일 크기를 줄이고 실행 속도를 높입니다.",
|
||||
sqlInsertModeSingle: "문 하나당 한 행",
|
||||
sqlInsertModeSingleDescription: "행마다 완전한 INSERT 문을 작성하여 개별 실행과 문제 해결을 쉽게 합니다.",
|
||||
exportTxt: "TXT 내보내기",
|
||||
exportSelectedRowsCsv: "선택한 행을 CSV로 내보내기",
|
||||
exportSelectedRowsXlsx: "선택한 행을 XLSX로 내보내기",
|
||||
|
||||
@@ -1603,6 +1603,12 @@ export default withEnglishFallback({
|
||||
exportJson: "Exportar JSON",
|
||||
exportMarkdown: "Exportar Markdown",
|
||||
exportSql: "Exportar SQL INSERT",
|
||||
sqlInsertModeTitle: "Modo de exportação SQL INSERT",
|
||||
sqlInsertModePrompt: "Escolha como as linhas serão gravadas no arquivo SQL INSERT:",
|
||||
sqlInsertModeBatch: "Adição em lote",
|
||||
sqlInsertModeBatchDescription: "Combina várias linhas em cada instrução INSERT para gerar arquivos menores e executar mais rapidamente.",
|
||||
sqlInsertModeSingle: "Uma linha por instrução",
|
||||
sqlInsertModeSingleDescription: "Escreve uma instrução INSERT completa por linha para facilitar a execução individual e a solução de problemas.",
|
||||
exportTxt: "Exportar TXT",
|
||||
exportSelectedRowsCsv: "Exportar Linhas Selecionadas como CSV",
|
||||
exportSelectedRowsXlsx: "Exportar Linhas Selecionadas como XLSX",
|
||||
|
||||
@@ -1645,6 +1645,12 @@ export default withEnglishFallback({
|
||||
exportJson: "导出 JSON",
|
||||
exportMarkdown: "导出 Markdown",
|
||||
exportSql: "导出 SQL INSERT",
|
||||
sqlInsertModeTitle: "SQL INSERT 导出模式",
|
||||
sqlInsertModePrompt: "请选择 SQL INSERT 文件的写出模式:",
|
||||
sqlInsertModeBatch: "批量追加",
|
||||
sqlInsertModeBatchDescription: "将多行数据合并到一条 INSERT 语句,文件更小且执行效率更高。",
|
||||
sqlInsertModeSingle: "每行独立",
|
||||
sqlInsertModeSingleDescription: "每行生成一条完整的 INSERT 语句,便于单独执行和排查。",
|
||||
exportTxt: "导出 TXT",
|
||||
exportSelectedRowsCsv: "导出选中行为 CSV",
|
||||
exportSelectedRowsXlsx: "导出选中行为 XLSX",
|
||||
|
||||
@@ -1601,6 +1601,12 @@ export default withEnglishFallback({
|
||||
exportJson: "匯出 JSON",
|
||||
exportMarkdown: "匯出 Markdown",
|
||||
exportSql: "匯出 SQL INSERT",
|
||||
sqlInsertModeTitle: "SQL INSERT 匯出模式",
|
||||
sqlInsertModePrompt: "請選擇 SQL INSERT 檔案的資料列寫入方式:",
|
||||
sqlInsertModeBatch: "批次追加",
|
||||
sqlInsertModeBatchDescription: "將多筆資料合併到每個 INSERT 陳述式中,讓檔案更小、執行更快。",
|
||||
sqlInsertModeSingle: "每列一個陳述式",
|
||||
sqlInsertModeSingleDescription: "每列寫入一個完整的 INSERT 陳述式,方便個別執行與疑難排解。",
|
||||
exportTxt: "匯出 TXT",
|
||||
exportSelectedRowsCsv: "匯出選取資料列為 CSV",
|
||||
exportSelectedRowsXlsx: "匯出選取資料列為 XLSX",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { decodeMeilisearchDocumentPage, decodeMeilisearchSearchResult, type Meil
|
||||
import type { XuguTablespaceInfo } from "@/types/database";
|
||||
import type { CreatedKey, EnqueuedTaskSummary, KeyCreateInput, KeyListItem, KeyPage, KeyUpdateInput, MeilisearchSystemOverview, MeilisearchTask, TaskListInput, TaskPage, TaskSelector } from "@/types/meilisearchManagement";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
|
||||
/** Normalize Tauri rejections once at the public backend boundary. */
|
||||
async function invokeBackend<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
@@ -4843,6 +4844,7 @@ export interface TableExportRequest {
|
||||
tableName: string;
|
||||
filePath: string;
|
||||
format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt";
|
||||
insertMode?: SqlInsertMode;
|
||||
csvQuoteMode?: CsvQuoteMode;
|
||||
columns?: string[];
|
||||
columnTypes?: Array<string | null | undefined>;
|
||||
@@ -4892,6 +4894,7 @@ export interface QueryResultExportRequest {
|
||||
useAgentCursor: boolean;
|
||||
filePath: string;
|
||||
format: "csv" | "xlsx" | "txt" | "sql";
|
||||
insertMode?: SqlInsertMode;
|
||||
csvQuoteMode?: CsvQuoteMode;
|
||||
includeSqlSheet?: boolean;
|
||||
pageSize: number;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { escapeCsvField, type CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
|
||||
export type ExportCellValue = string | number | boolean | null;
|
||||
|
||||
@@ -38,11 +39,12 @@ export interface FormatSqlInsertOptions {
|
||||
spatialColumns?: QueryResult["spatial_columns"];
|
||||
spatialValues?: QueryResult["spatial_values"];
|
||||
rows: ExportCellValue[][];
|
||||
insertMode?: SqlInsertMode;
|
||||
}
|
||||
|
||||
export function formatSqlInsert(options: FormatSqlInsertOptions): Promise<string> {
|
||||
export function formatSqlInsert({ insertMode = "batch", ...options }: FormatSqlInsertOptions): Promise<string> {
|
||||
return api.buildExportSqlInsert({
|
||||
...options,
|
||||
batchSize: 1,
|
||||
batchSize: insertMode === "single" ? 1 : undefined,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createApp } from "vue";
|
||||
import SqlInsertModeDialog from "@/components/export/SqlInsertModeDialog.vue";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
export type SqlInsertMode = "batch" | "single";
|
||||
|
||||
export const DEFAULT_SQL_INSERT_MODE: SqlInsertMode = "batch";
|
||||
|
||||
export function showSqlInsertModeDialog(): Promise<SqlInsertMode | null> {
|
||||
if (typeof document === "undefined") return Promise.resolve(DEFAULT_SQL_INSERT_MODE);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
let settled = false;
|
||||
let app: ReturnType<typeof createApp> | null = null;
|
||||
const finish = (value: SqlInsertMode | null) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(value);
|
||||
app?.unmount();
|
||||
container.remove();
|
||||
};
|
||||
app = createApp(SqlInsertModeDialog, {
|
||||
open: true,
|
||||
onConfirm: (insertMode: SqlInsertMode) => finish(insertMode),
|
||||
onCancel: () => finish(null),
|
||||
});
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
});
|
||||
}
|
||||
@@ -77,6 +77,31 @@ describe("queryStore query result export", () => {
|
||||
expect(mocks.connectionIdentifierQuote).toHaveBeenCalledWith("kingbase-1");
|
||||
});
|
||||
|
||||
it("passes the selected SQL INSERT mode to the backend request", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("kingbase-1", "app", "Query");
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
tab.sql = "SELECT id, name FROM users";
|
||||
tab.lastExecutedSql = tab.sql;
|
||||
tab.result = {
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, "Ada"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
};
|
||||
|
||||
const request = await store.buildQueryResultExportRequest(tabId, {
|
||||
exportId: "export-single",
|
||||
filePath: "users.sql",
|
||||
format: "sql",
|
||||
exportTableName: "users",
|
||||
insertMode: "single",
|
||||
});
|
||||
|
||||
expect(request?.insertMode).toBe("single");
|
||||
});
|
||||
|
||||
it("uses the Agent cursor for SQL Server legacy result export", async () => {
|
||||
mocks.getConfig.mockReturnValue({
|
||||
id: "sqlserver-2000",
|
||||
|
||||
@@ -41,6 +41,7 @@ import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport";
|
||||
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
|
||||
import { isDataTabMetadataLifecycleStale } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||
import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { getCachedTableMetadata, loadTableColumns, loadTableIndexes, loadTableMetadata, tableMetadataToDataTabMeta, updateCachedTableMetadataType, type TableMetadataRequest } from "@/lib/metadata/tableMetadataCache";
|
||||
import { MetadataTaskLimiter } from "@/lib/metadata/metadataTaskLimiter";
|
||||
@@ -149,6 +150,7 @@ interface BuildQueryResultExportRequestOptions {
|
||||
includeSqlSheet?: boolean;
|
||||
exportTableName?: string;
|
||||
exportColumnTypes?: Array<string | null | undefined>;
|
||||
insertMode?: SqlInsertMode;
|
||||
}
|
||||
|
||||
interface OpenSavedSqlOptions {
|
||||
@@ -7558,6 +7560,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
useAgentCursor,
|
||||
filePath: options.filePath,
|
||||
format: options.format,
|
||||
...(options.format === "sql" && options.insertMode ? { insertMode: options.insertMode } : {}),
|
||||
includeSqlSheet: options.format === "xlsx" && options.includeSqlSheet === true,
|
||||
pageSize: settings.exportBatchSize,
|
||||
rowLimit,
|
||||
|
||||
Reference in new issue
Block a user