feat(export): split large sql exports into zip parts
This commit is contained in:
1 parent
339c96cc81
commit
16866293a3
30 files changed
+933
-57
No files matched your search
@@ -68,6 +68,10 @@ const includeObjects = ref(true);
|
||||
const includeCreateDatabase = ref(false);
|
||||
const dropTableIfExists = ref(false);
|
||||
const omitAutoIncrement = ref(false);
|
||||
const splitSqlOutput = ref(false);
|
||||
const splitSqlPartMaxMb = ref(100);
|
||||
const MIN_SPLIT_SQL_PART_MB = 1;
|
||||
const MAX_SPLIT_SQL_PART_MB = 4096;
|
||||
// `AUTO_INCREMENT` stripping is a MySQL-only DDL transform (backend gates on
|
||||
// db_type == mysql, which also covers MariaDB / TiDB / OceanBase-MySQL-mode).
|
||||
const isMysqlFamily = computed(() => store.getConfig(connectionId.value)?.db_type === "mysql");
|
||||
@@ -161,6 +165,12 @@ function joinExportPath(directory: string, fileName: string): string {
|
||||
return `${directory.replace(/[\\/]+$/, "")}${separator}${fileName}`;
|
||||
}
|
||||
|
||||
function normalizedSplitSqlPartMaxMb(): number {
|
||||
const value = Number(splitSqlPartMaxMb.value);
|
||||
if (!Number.isFinite(value)) return 100;
|
||||
return Math.min(MAX_SPLIT_SQL_PART_MB, Math.max(MIN_SPLIT_SQL_PART_MB, Math.round(value)));
|
||||
}
|
||||
|
||||
// Lenient exports write per-object failures into the SQL file as `-- ERROR`
|
||||
// comments and still finish; completion must warn instead of reporting plain
|
||||
// success (#8184).
|
||||
@@ -329,8 +339,8 @@ async function startExport() {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const safeName = sanitizeFileName(database.value || "database");
|
||||
const path = await save({
|
||||
defaultPath: `${safeName}.sql`,
|
||||
filters: [{ name: "SQL", extensions: ["sql"] }],
|
||||
defaultPath: `${safeName}.${splitSqlOutput.value ? "zip" : "sql"}`,
|
||||
filters: [{ name: splitSqlOutput.value ? "ZIP" : "SQL", extensions: [splitSqlOutput.value ? "zip" : "sql"] }],
|
||||
});
|
||||
if (!path) return;
|
||||
filePath = path;
|
||||
@@ -392,6 +402,7 @@ async function startExport() {
|
||||
omitAutoIncrement: omitAutoIncrement.value,
|
||||
snapshotSessionId,
|
||||
batchSize: 1000,
|
||||
splitMaxMb: splitSqlOutput.value ? normalizedSplitSqlPartMaxMb() : undefined,
|
||||
};
|
||||
return runDatabaseExportUntilTerminal(request, (progress) => {
|
||||
exportProgress.value = { ...progress };
|
||||
@@ -508,7 +519,7 @@ async function startAllDatabasesExport() {
|
||||
batchDatabaseIndex.value = index + 1;
|
||||
const currentExportId = `${batchId}-${index + 1}`;
|
||||
activeDatabaseExportId.value = currentExportId;
|
||||
const filePath = isTauriRuntime() ? joinExportPath(directoryPath, `${sanitizeFileName(item.fileStem)}.sql`) : `__web_export_${currentExportId}.sql`;
|
||||
const filePath = isTauriRuntime() ? joinExportPath(directoryPath, `${sanitizeFileName(item.fileStem)}.${splitSqlOutput.value ? "zip" : "sql"}`) : `__web_export_${currentExportId}.${splitSqlOutput.value ? "zip" : "sql"}`;
|
||||
let currentDatabaseRowsExported = 0;
|
||||
|
||||
const terminal = await runWithDatabaseBackupSnapshot(
|
||||
@@ -533,6 +544,7 @@ async function startAllDatabasesExport() {
|
||||
omitAutoIncrement: omitAutoIncrement.value,
|
||||
snapshotSessionId,
|
||||
batchSize: 1000,
|
||||
splitMaxMb: splitSqlOutput.value ? normalizedSplitSqlPartMaxMb() : undefined,
|
||||
},
|
||||
(progress) => {
|
||||
const nextRowsExported = Math.max(0, progress.rowsExported);
|
||||
@@ -651,6 +663,8 @@ function resetState() {
|
||||
includeCreateDatabase.value = false;
|
||||
dropTableIfExists.value = false;
|
||||
omitAutoIncrement.value = false;
|
||||
splitSqlOutput.value = false;
|
||||
splitSqlPartMaxMb.value = 100;
|
||||
isExporting.value = false;
|
||||
exportProgress.value = null;
|
||||
exportDone.value = false;
|
||||
@@ -938,6 +952,27 @@ watch(
|
||||
<Square v-else class="w-3.5 h-3.5 text-muted-foreground/40 shrink-0" />
|
||||
{{ t("databaseExport.includeObjects") }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 text-xs">
|
||||
<button type="button" class="flex min-w-0 items-center gap-2 text-left" @click="splitSqlOutput = !splitSqlOutput">
|
||||
<CheckSquare v-if="splitSqlOutput" class="w-3.5 h-3.5 text-primary shrink-0" />
|
||||
<Square v-else class="w-3.5 h-3.5 text-muted-foreground/40 shrink-0" />
|
||||
<span>{{ t("databaseExport.splitSqlOutput") }}</span>
|
||||
</button>
|
||||
<Input
|
||||
v-if="splitSqlOutput"
|
||||
v-model.number="splitSqlPartMaxMb"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:min="MIN_SPLIT_SQL_PART_MB"
|
||||
:max="MAX_SPLIT_SQL_PART_MB"
|
||||
class="h-7 w-24 text-xs"
|
||||
:aria-label="t('databaseExport.splitSqlPartMaxMb')"
|
||||
@blur="splitSqlPartMaxMb = normalizedSplitSqlPartMaxMb()"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="splitSqlOutput" class="pl-5 text-[11px] text-muted-foreground">
|
||||
{{ t("databaseExport.splitSqlOutputDescription", { min: MIN_SPLIT_SQL_PART_MB, max: MAX_SPLIT_SQL_PART_MB }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,22 +3,36 @@ 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";
|
||||
import type { SqlExportOptions, SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
|
||||
const { t } = useI18n();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const props = defineProps<{ allowSplit?: boolean }>();
|
||||
const selected = ref<SqlInsertMode>("batch");
|
||||
const splitSqlOutput = ref(false);
|
||||
const splitSqlPartMaxMb = ref(100);
|
||||
const MIN_SPLIT_SQL_PART_MB = 1;
|
||||
const MAX_SPLIT_SQL_PART_MB = 4096;
|
||||
let outcomeEmitted = false;
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [mode: SqlInsertMode];
|
||||
confirm: [options: SqlExportOptions];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
function normalizedSplitSqlPartMaxMb(): number {
|
||||
const value = Number(splitSqlPartMaxMb.value);
|
||||
if (!Number.isFinite(value)) return 100;
|
||||
return Math.min(MAX_SPLIT_SQL_PART_MB, Math.max(MIN_SPLIT_SQL_PART_MB, Math.round(value)));
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
outcomeEmitted = true;
|
||||
open.value = false;
|
||||
emit("confirm", selected.value);
|
||||
emit("confirm", {
|
||||
insertMode: selected.value,
|
||||
splitMaxMb: props.allowSplit && splitSqlOutput.value ? normalizedSplitSqlPartMaxMb() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
@@ -55,6 +69,19 @@ function onOpenChange(value: boolean) {
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("grid.sqlInsertModeSingleDescription") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="props.allowSplit" class="space-y-2 rounded-md border p-3">
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input v-model="splitSqlOutput" type="checkbox" class="h-4 w-4" data-sql-split-output />
|
||||
{{ t("databaseExport.splitSqlOutput") }}
|
||||
</label>
|
||||
<div v-if="splitSqlOutput" class="flex items-center justify-between gap-3 pl-6">
|
||||
<span class="text-xs text-muted-foreground">{{ t("databaseExport.splitSqlPartMaxMb") }}</span>
|
||||
<input v-model.number="splitSqlPartMaxMb" type="number" :min="MIN_SPLIT_SQL_PART_MB" :max="MAX_SPLIT_SQL_PART_MB" class="h-8 w-24 rounded-md border bg-background px-2 text-sm" data-sql-split-max-mb />
|
||||
</div>
|
||||
<p v-if="splitSqlOutput" class="pl-6 text-xs text-muted-foreground">
|
||||
{{ t("databaseExport.splitSqlOutputDescription", { min: MIN_SPLIT_SQL_PART_MB, max: MAX_SPLIT_SQL_PART_MB }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="onCancel">{{ t("common.cancel") }}</Button>
|
||||
|
||||
@@ -37,13 +37,13 @@ afterEach(() => {
|
||||
i18n.global.locale.value = "en";
|
||||
});
|
||||
|
||||
async function mountDialog(onConfirm = () => {}, onCancel = () => {}) {
|
||||
async function mountDialog(onConfirm = () => {}, onCancel = () => {}, allowSplit = false) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(SqlInsertModeDialog, { open: true, onConfirm, onCancel });
|
||||
return () => h(SqlInsertModeDialog, { open: true, allowSplit, onConfirm, onCancel });
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -68,7 +68,24 @@ describe("SqlInsertModeDialog", () => {
|
||||
document.querySelector<HTMLButtonElement>("[data-sql-insert-mode-confirm]")?.click();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
expect(onConfirm).toHaveBeenCalledWith("single");
|
||||
expect(onConfirm).toHaveBeenCalledWith({ insertMode: "single", splitMaxMb: undefined });
|
||||
});
|
||||
|
||||
it("enables split ZIP output only when the caller supports it", async () => {
|
||||
const onConfirm = vi.fn();
|
||||
await mountDialog(onConfirm, () => {}, true);
|
||||
|
||||
const split = document.querySelector<HTMLInputElement>("input[data-sql-split-output]");
|
||||
expect(split).not.toBeNull();
|
||||
split?.click();
|
||||
await nextTick();
|
||||
const size = document.querySelector<HTMLInputElement>("input[data-sql-split-max-mb]");
|
||||
expect(size?.value).toBe("100");
|
||||
if (size) size.value = "256";
|
||||
size?.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
document.querySelector<HTMLButtonElement>("[data-sql-insert-mode-confirm]")?.click();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith({ insertMode: "batch", splitMaxMb: 256 });
|
||||
});
|
||||
|
||||
it("renders the batch and single-row explanations", async () => {
|
||||
|
||||
@@ -2112,9 +2112,9 @@ async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql")
|
||||
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");
|
||||
const sqlExportOptions = format === "sql" ? await showSqlInsertModeDialog({ allowSplit: true }) : undefined;
|
||||
if (format === "sql" && sqlExportOptions === null) return;
|
||||
await exportTableData(row, format, undefined, "name", true, sqlExportOptions?.insertMode ?? "batch", sqlExportOptions?.splitMaxMb);
|
||||
}
|
||||
|
||||
function showObjectBrowserXlsxHeaderDialog(hasComments: boolean): Promise<XlsxExportOptions | null> {
|
||||
@@ -2157,17 +2157,18 @@ 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, insertMode: SqlInsertMode = "batch") {
|
||||
async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "sql", columnInfos?: ColumnInfo[], headerMode: XlsxHeaderMode = "name", autoFilter = true, insertMode: SqlInsertMode = "batch", splitMaxMb?: number) {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
const splitSqlOutput = format === "sql" && splitMaxMb !== undefined;
|
||||
|
||||
// Save dialog first
|
||||
let filePath = "";
|
||||
const defaultName = `${row.name}.${format}`;
|
||||
const defaultName = `${row.name}.${splitSqlOutput ? "zip" : format}`;
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
try {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const filter = format === "csv" ? { name: "CSV", extensions: ["csv"] } : format === "xlsx" ? { name: "Excel", extensions: ["xlsx"] } : { name: "SQL", extensions: ["sql"] };
|
||||
const filter = format === "csv" ? { name: "CSV", extensions: ["csv"] } : format === "xlsx" ? { name: "Excel", extensions: ["xlsx"] } : splitSqlOutput ? { name: "ZIP", extensions: ["zip"] } : { name: "SQL", extensions: ["sql"] };
|
||||
const path = await save({
|
||||
defaultPath: defaultName,
|
||||
filters: [filter],
|
||||
@@ -2180,7 +2181,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
}
|
||||
} else {
|
||||
const webExportId = generateDatabaseExportId();
|
||||
filePath = `__web_export_${webExportId}.${format}`;
|
||||
filePath = `__web_export_${webExportId}.${splitSqlOutput ? "zip" : format}`;
|
||||
}
|
||||
|
||||
let task: ExportTask | null = null;
|
||||
@@ -2231,7 +2232,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
tableName: row.name,
|
||||
filePath,
|
||||
format,
|
||||
...(format === "sql" ? { insertMode } : {}),
|
||||
...(format === "sql" ? { insertMode, splitMaxMb } : {}),
|
||||
csvQuoteMode: settingsStore.editorSettings.csvQuoteMode,
|
||||
columns,
|
||||
columnComments: format === "xlsx" ? columnComments : undefined,
|
||||
|
||||
@@ -30,9 +30,9 @@ describe("ObjectBrowser XLSX export", () => {
|
||||
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 } : {})');
|
||||
expect(exportData).toContain("await showSqlInsertModeDialog({ allowSplit: true })");
|
||||
expect(exportData.indexOf("await showSqlInsertModeDialog({ allowSplit: true })")).toBeLessThan(exportData.indexOf("await exportTableData("));
|
||||
expect(exportTableData).toContain('...(format === "sql" ? { insertMode, splitMaxMb } : {})');
|
||||
});
|
||||
|
||||
it("falls back to field-name headers when column metadata is unavailable", () => {
|
||||
|
||||
@@ -15,7 +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 { showSqlInsertModeDialog, type SqlExportOptions, type SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
import { summarizeExportRows } from "@/lib/export/exportDiagnostics";
|
||||
import { appendDebugLog, appendNativeProcessMemoryLog, getBrowserMemorySnapshot, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
@@ -1164,7 +1164,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, insertMode?: SqlInsertMode): Promise<boolean> {
|
||||
async function exportFullTableDataViaBackend(format: "csv" | "xlsx" | "json" | "markdown" | "sql" | "txt", rowIds?: number[], headerMode: XlsxHeaderMode = "name", autoFilter = true, sqlExportOptions?: SqlExportOptions): 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.
|
||||
@@ -1173,8 +1173,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
const fmt = FORMAT_META[format];
|
||||
const extension = fmt?.ext ?? format;
|
||||
const filterName = fmt?.label ?? format.toUpperCase();
|
||||
const splitSqlOutput = format === "sql" && sqlExportOptions?.splitMaxMb !== undefined;
|
||||
const extension = splitSqlOutput ? "zip" : (fmt?.ext ?? format);
|
||||
const filterName = splitSqlOutput ? "ZIP" : (fmt?.label ?? format.toUpperCase());
|
||||
let outputPath = exportFileName(meta.tableName || "export", extension, { preferFallback: true });
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
@@ -1223,7 +1224,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
tableName: meta.tableName,
|
||||
filePath: outputPath,
|
||||
format,
|
||||
...(format === "sql" && insertMode ? { insertMode } : {}),
|
||||
...(format === "sql" && sqlExportOptions ? { insertMode: sqlExportOptions.insertMode, splitMaxMb: sqlExportOptions.splitMaxMb } : {}),
|
||||
csvQuoteMode: editorSettings.csvQuoteMode,
|
||||
columns: columns.value,
|
||||
columnTypes: columnTypes.value,
|
||||
@@ -1409,16 +1410,18 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
},
|
||||
true,
|
||||
);
|
||||
const insertMode = await showSqlInsertModeDialog();
|
||||
if (insertMode === null) {
|
||||
const selectedSqlExportOptions = (await showSqlInsertModeDialog({ allowSplit: rowIds === undefined && context.value === "table-data" })) as SqlExportOptions | SqlInsertMode | null;
|
||||
if (selectedSqlExportOptions === null) {
|
||||
logExportStage("cancelled", { stage: "insert-mode-dialog" });
|
||||
return;
|
||||
}
|
||||
logExportStage("mode-selected", { insertMode });
|
||||
const sqlExportOptions = typeof selectedSqlExportOptions === "string" ? { insertMode: selectedSqlExportOptions } : selectedSqlExportOptions;
|
||||
const insertMode = sqlExportOptions.insertMode;
|
||||
logExportStage("mode-selected", { insertMode, splitMaxMb: sqlExportOptions.splitMaxMb });
|
||||
try {
|
||||
// Step 1: table-data context — existing backend table export
|
||||
logExportStage("backend-export-start");
|
||||
const handledByBackend = await exportFullTableDataViaBackend("sql", rowIds, "name", true, insertMode);
|
||||
const handledByBackend = await exportFullTableDataViaBackend("sql", rowIds, "name", true, sqlExportOptions);
|
||||
logExportStage("backend-export-finished", { handledByBackend });
|
||||
if (handledByBackend) {
|
||||
logExportStage("done", { path: "backend" });
|
||||
@@ -1490,8 +1493,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
|
||||
async function exportCurrentPageSql() {
|
||||
await runExclusiveExport(async () => {
|
||||
const insertMode = await showSqlInsertModeDialog();
|
||||
if (insertMode === null) return;
|
||||
const selectedSqlExportOptions = (await showSqlInsertModeDialog()) as SqlExportOptions | SqlInsertMode | null;
|
||||
if (selectedSqlExportOptions === null) return;
|
||||
const insertMode = typeof selectedSqlExportOptions === "string" ? selectedSqlExportOptions : selectedSqlExportOptions.insertMode;
|
||||
try {
|
||||
const result = await resultToExport(undefined, undefined, false, false);
|
||||
const exportData = sqlInsertExportData(result);
|
||||
|
||||
@@ -7881,6 +7881,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "AUTO_INCREMENT parametrini daxil etmə (yeni quraşdırma skriptləri üçün)",
|
||||
includeData: "Cədvəl məlumatları (INSERT)",
|
||||
includeObjects: "Görünüşlər / prosedurlar / funksiyalar / ardıcıllıqlar",
|
||||
splitSqlOutput: "SQL ixracını ZIP arxivinə böl",
|
||||
splitSqlPartMaxMb: "Hər SQL hissəsi üçün maksimum ölçü (MB)",
|
||||
splitSqlOutputDescription: "Nömrələnmiş SQL hissələri və manifestdən ibarət ZIP arxivi yaradır. Hissələr tam SQL ifadələri arasında bölünür ({min}–{max} MB).",
|
||||
databaseSelection: "Verilənlər bazaları",
|
||||
selectedDatabases: "{selected}/{total} seçilib",
|
||||
selectAllDatabases: "Hamısını seç",
|
||||
|
||||
@@ -7879,6 +7879,9 @@ export default {
|
||||
omitAutoIncrement: "Omit AUTO_INCREMENT (for fresh-install scripts)",
|
||||
includeData: "Table data (INSERT)",
|
||||
includeObjects: "Views / Procedures / Functions / Sequences",
|
||||
splitSqlOutput: "Split SQL export into a ZIP archive",
|
||||
splitSqlPartMaxMb: "Maximum size per SQL part (MB)",
|
||||
splitSqlOutputDescription: "Creates a ZIP archive containing numbered SQL parts and a manifest. Parts are split between complete SQL statements ({min}–{max} MB).",
|
||||
databaseSelection: "Databases",
|
||||
selectedDatabases: "{selected}/{total} selected",
|
||||
selectAllDatabases: "Select all",
|
||||
|
||||
@@ -7441,6 +7441,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "Omitir AUTO_INCREMENT (para scripts de instalación nueva)",
|
||||
includeData: "Datos de tablas (INSERT)",
|
||||
includeObjects: "Vistas / Procedimientos / Funciones / Secuencias",
|
||||
splitSqlOutput: "Dividir la exportación SQL en un archivo ZIP",
|
||||
splitSqlPartMaxMb: "Tamaño máximo por parte SQL (MB)",
|
||||
splitSqlOutputDescription: "Crea un archivo ZIP con partes SQL numeradas y un manifiesto. Las partes se dividen entre sentencias SQL completas ({min}–{max} MB).",
|
||||
databaseSelection: "Bases de datos",
|
||||
selectedDatabases: "{selected}/{total} seleccionadas",
|
||||
selectAllDatabases: "Seleccionar todo",
|
||||
|
||||
@@ -7442,6 +7442,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "Ometti AUTO_INCREMENT (per script di installazione pulita)",
|
||||
includeData: "Dati tabella (INSERT)",
|
||||
includeObjects: "Viste / Procedure / Funzioni / Sequenze",
|
||||
splitSqlOutput: "Dividere l'esportazione SQL in un archivio ZIP",
|
||||
splitSqlPartMaxMb: "Dimensione massima per parte SQL (MB)",
|
||||
splitSqlOutputDescription: "Crea un archivio ZIP con parti SQL numerate e un manifest. Le parti vengono divise tra istruzioni SQL complete ({min}–{max} MB).",
|
||||
databaseSelection: "Database",
|
||||
selectedDatabases: "{selected}/{total} selezionati",
|
||||
selectAllDatabases: "Seleziona tutti",
|
||||
|
||||
@@ -7494,6 +7494,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "AUTO_INCREMENT を省略(新規インストール用スクリプト向け)",
|
||||
includeData: "テーブルデータ (INSERT)",
|
||||
includeObjects: "ビュー / プロシージャ / 関数 / シーケンス",
|
||||
splitSqlOutput: "SQLエクスポートをZIPアーカイブに分割",
|
||||
splitSqlPartMaxMb: "SQLパートあたりの最大サイズ(MB)",
|
||||
splitSqlOutputDescription: "番号付きSQLパートとマニフェストを含むZIPアーカイブを作成します。パートは完全なSQL文の間で分割されます({min}~{max} MB)。",
|
||||
databaseSelection: "データベース",
|
||||
selectedDatabases: "{selected}/{total}件選択中",
|
||||
selectAllDatabases: "すべて選択",
|
||||
|
||||
@@ -7160,6 +7160,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "AUTO_INCREMENT 생략 (초기 설치 스크립트용)",
|
||||
includeData: "테이블 데이터 (INSERT)",
|
||||
includeObjects: "뷰 / 프로시저 / 함수 / 시퀀스",
|
||||
splitSqlOutput: "SQL 내보내기를 ZIP 압축 파일로 분할",
|
||||
splitSqlPartMaxMb: "SQL 파트당 최대 크기(MB)",
|
||||
splitSqlOutputDescription: "번호가 지정된 SQL 파트와 매니페스트가 포함된 ZIP 압축 파일을 만듭니다. 파트는 완전한 SQL 문 사이에서 분할됩니다({min}–{max} MB).",
|
||||
databaseSelection: "데이터베이스",
|
||||
selectedDatabases: "{selected}/{total} 선택됨",
|
||||
selectAllDatabases: "모두 선택",
|
||||
|
||||
@@ -7443,6 +7443,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "Omitir AUTO_INCREMENT (para scripts de instalação do zero)",
|
||||
includeData: "Dados da tabela (INSERT)",
|
||||
includeObjects: "Views / Procedures / Funções / Sequências",
|
||||
splitSqlOutput: "Dividir exportação SQL em arquivo ZIP",
|
||||
splitSqlPartMaxMb: "Tamanho máximo por parte SQL (MB)",
|
||||
splitSqlOutputDescription: "Cria um arquivo ZIP com partes SQL numeradas e um manifesto. As partes são divididas entre instruções SQL completas ({min}–{max} MB).",
|
||||
databaseSelection: "Bancos de dados",
|
||||
selectedDatabases: "{selected}/{total} selecionados",
|
||||
selectAllDatabases: "Selecionar todos",
|
||||
|
||||
@@ -7733,6 +7733,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "AUTO_INCREMENT'i atla (sıfırdan kurulum betikleri için)",
|
||||
includeData: "Tablo verisi (INSERT)",
|
||||
includeObjects: "Görünümler / Yordamlar / İşlevler / Diziler",
|
||||
splitSqlOutput: "SQL dışa aktarmayı ZIP arşivine böl",
|
||||
splitSqlPartMaxMb: "SQL parçası başına maksimum boyut (MB)",
|
||||
splitSqlOutputDescription: "Numaralı SQL parçaları ve bir bildirim içeren bir ZIP arşivi oluşturur. Parçalar tam SQL ifadeleri arasında bölünür ({min}–{max} MB).",
|
||||
databaseSelection: "Veritabanları",
|
||||
selectedDatabases: "{selected}/{total} seçildi",
|
||||
selectAllDatabases: "Tümünü seç",
|
||||
|
||||
@@ -7860,6 +7860,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "省略 AUTO_INCREMENT(用于全新安装的初始化脚本)",
|
||||
includeData: "表数据 (INSERT)",
|
||||
includeObjects: "视图 / 存储过程 / 函数 / 序列",
|
||||
splitSqlOutput: "将 SQL 导出拆分为 ZIP 压缩包",
|
||||
splitSqlPartMaxMb: "每个 SQL 分片的最大大小(MB)",
|
||||
splitSqlOutputDescription: "创建包含编号 SQL 分片和清单的 ZIP 压缩包。分片仅在完整 SQL 语句之间拆分({min}–{max} MB)。",
|
||||
databaseSelection: "选择数据库",
|
||||
selectedDatabases: "已选择 {selected}/{total}",
|
||||
selectAllDatabases: "全选",
|
||||
|
||||
@@ -6757,6 +6757,9 @@ export default withEnglishFallback({
|
||||
omitAutoIncrement: "省略 AUTO_INCREMENT(用於全新安裝的初始化腳本)",
|
||||
includeData: "資料表資料 (INSERT)",
|
||||
includeObjects: "檢視 / 預存程序 / 函式 / 序列",
|
||||
splitSqlOutput: "將 SQL 匯出分割為 ZIP 壓縮檔",
|
||||
splitSqlPartMaxMb: "每個 SQL 分片的最大大小(MB)",
|
||||
splitSqlOutputDescription: "建立包含編號 SQL 分片與資訊清單的 ZIP 壓縮檔。分片只會在完整 SQL 陳述式之間分割({min}–{max} MB)。",
|
||||
databaseSelection: "選擇資料庫",
|
||||
selectedDatabases: "已選擇 {selected}/{total}",
|
||||
selectAllDatabases: "全選",
|
||||
|
||||
@@ -4891,6 +4891,7 @@ export interface DatabaseExportRequest {
|
||||
outputCompression?: "none" | "gzip";
|
||||
snapshotSessionId?: string;
|
||||
batchSize: number;
|
||||
splitMaxMb?: number;
|
||||
}
|
||||
|
||||
export interface DatabaseBackupSnapshot {
|
||||
@@ -4941,6 +4942,7 @@ export interface TableExportRequest {
|
||||
dateTimeFormat?: string;
|
||||
numericColumnRightAlign?: boolean;
|
||||
autoFilter?: boolean;
|
||||
splitMaxMb?: number;
|
||||
}
|
||||
|
||||
export interface TableCsvExportOptions {
|
||||
|
||||
@@ -4,17 +4,22 @@ import i18n from "@/i18n";
|
||||
|
||||
export type SqlInsertMode = "batch" | "single";
|
||||
|
||||
export interface SqlExportOptions {
|
||||
insertMode: SqlInsertMode;
|
||||
splitMaxMb?: number;
|
||||
}
|
||||
|
||||
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);
|
||||
export function showSqlInsertModeDialog(options: { allowSplit?: boolean } = {}): Promise<SqlExportOptions | null> {
|
||||
if (typeof document === "undefined") return Promise.resolve({ insertMode: 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) => {
|
||||
const finish = (value: SqlExportOptions | null) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(value);
|
||||
@@ -23,7 +28,8 @@ export function showSqlInsertModeDialog(): Promise<SqlInsertMode | null> {
|
||||
};
|
||||
app = createApp(SqlInsertModeDialog, {
|
||||
open: true,
|
||||
onConfirm: (insertMode: SqlInsertMode) => finish(insertMode),
|
||||
allowSplit: options.allowSplit === true,
|
||||
onConfirm: (options: SqlExportOptions) => finish(options),
|
||||
onCancel: () => finish(null),
|
||||
});
|
||||
app.use(i18n);
|
||||
|
||||
Reference in new issue
Block a user