feat(export): remember last export options and sort tables by name

This commit is contained in:
lxk955 authored and GitHub committed 2026-10-07 00:14:32 +08:00
1 parent 4e144035b1
commit 7f60b43b43
3 files changed
+219 -25

No files matched your search

@@ -24,6 +24,7 @@ import { isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
import type { SqlInsertDialect, SqlInsertMode } from "@/lib/export/sqlInsertMode";
import { revealExportedPath } from "@/lib/export/exportPath";
import { translateBackendError } from "@/i18n/backend-errors";
import { loadSavedDatabaseExportOptions, MAX_SPLIT_SQL_PART_MB, MIN_SPLIT_SQL_PART_MB, saveDatabaseExportOptions, sortDatabaseTableNames } from "@/lib/export/databaseExportOptions";
const { t } = useI18n();
const { toast } = useToast();
@@ -66,19 +67,49 @@ const tableError = ref<string | null>(null);
const POSTGRES_ALL_SCHEMAS = "__DBX_ALL_SCHEMAS__";
// Options
const includeStructure = ref(true);
const includeData = ref(true);
const insertDialect = ref<SqlInsertDialect>("source");
const insertMode = ref<SqlInsertMode>("batch");
const includeObjects = ref(true);
const includeCreateDatabase = ref(false);
const dropTableIfExists = ref(false);
const omitAutoIncrement = ref(false);
const preserveOriginalLanguage = ref(false);
const splitSqlOutput = ref(false);
const splitSqlPartMaxMb = ref(100);
const MIN_SPLIT_SQL_PART_MB = 1;
const MAX_SPLIT_SQL_PART_MB = 4096;
const savedOptions = loadSavedDatabaseExportOptions();
const includeStructure = ref(savedOptions.includeStructure);
const includeData = ref(savedOptions.includeData);
const insertDialect = ref<SqlInsertDialect>(savedOptions.insertDialect);
const insertMode = ref<SqlInsertMode>(savedOptions.insertMode);
const includeObjects = ref(savedOptions.includeObjects);
const includeCreateDatabase = ref(savedOptions.includeCreateDatabase);
const dropTableIfExists = ref(savedOptions.dropTableIfExists);
const omitAutoIncrement = ref(savedOptions.omitAutoIncrement);
const preserveOriginalLanguage = ref(savedOptions.preserveOriginalLanguage);
const splitSqlOutput = ref(savedOptions.splitSqlOutput);
const splitSqlPartMaxMb = ref(savedOptions.splitSqlPartMaxMb);
function applyStoredExportOptions() {
const saved = loadSavedDatabaseExportOptions();
includeStructure.value = saved.includeStructure;
includeData.value = saved.includeData;
insertDialect.value = saved.insertDialect;
insertMode.value = saved.insertMode;
includeObjects.value = saved.includeObjects;
includeCreateDatabase.value = saved.includeCreateDatabase;
dropTableIfExists.value = saved.dropTableIfExists;
omitAutoIncrement.value = saved.omitAutoIncrement;
preserveOriginalLanguage.value = saved.preserveOriginalLanguage;
splitSqlOutput.value = saved.splitSqlOutput;
splitSqlPartMaxMb.value = saved.splitSqlPartMaxMb;
}
function persistExportOptions() {
saveDatabaseExportOptions({
includeStructure: includeStructure.value,
includeData: includeData.value,
insertDialect: insertDialect.value,
insertMode: insertMode.value,
includeObjects: includeObjects.value,
includeCreateDatabase: includeCreateDatabase.value,
dropTableIfExists: dropTableIfExists.value,
omitAutoIncrement: omitAutoIncrement.value,
preserveOriginalLanguage: preserveOriginalLanguage.value,
splitSqlOutput: splitSqlOutput.value,
splitSqlPartMaxMb: normalizedSplitSqlPartMaxMb(),
});
}
// `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");
@@ -280,7 +311,7 @@ async function loadTables(preferredTable = "", preferredTables: string[] = []) {
selectedTables.value = [];
try {
const tableInfos = await api.listTables(connectionId.value, database.value, schema.value);
const names = tableInfos.map((table) => table.name);
const names = sortDatabaseTableNames(tableInfos.map((table) => table.name));
tables.value = names;
const preferredSet = new Set(preferredTables.filter((name) => names.includes(name)));
selectedTables.value = preferredSet.size > 0 ? names.filter((name) => preferredSet.has(name)) : preferredTable && names.includes(preferredTable) ? [preferredTable] : [...names];
@@ -687,17 +718,7 @@ function resetState() {
exportAllDatabases.value = false;
selectedDatabases.value = [];
databaseFilter.value = "";
includeStructure.value = true;
includeData.value = true;
insertDialect.value = "source";
insertMode.value = "batch";
includeObjects.value = true;
includeCreateDatabase.value = false;
dropTableIfExists.value = false;
omitAutoIncrement.value = false;
preserveOriginalLanguage.value = false;
splitSqlOutput.value = false;
splitSqlPartMaxMb.value = 100;
applyStoredExportOptions();
isExporting.value = false;
exportProgress.value = null;
exportDone.value = false;
@@ -820,6 +841,12 @@ watch(
},
{ immediate: true },
);
watch([includeStructure, includeData, insertDialect, insertMode, includeObjects, includeCreateDatabase, dropTableIfExists, omitAutoIncrement, preserveOriginalLanguage, splitSqlOutput, splitSqlPartMaxMb], () => {
if (open.value) {
persistExportOptions();
}
});
</script>
<template>
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_DATABASE_EXPORT_OPTIONS, DATABASE_EXPORT_OPTIONS_STORAGE_KEY, loadSavedDatabaseExportOptions, saveDatabaseExportOptions, sortDatabaseTableNames } from "../databaseExportOptions";
function createMemoryStorage(initial: Record<string, string> = {}) {
const store = new Map<string, string>(Object.entries(initial));
return {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => store.set(key, value),
removeItem: (key: string) => store.delete(key),
clear: () => store.clear(),
};
}
describe("databaseExportOptions", () => {
it("returns default options when storage is empty or contains invalid json", () => {
const emptyStorage = createMemoryStorage();
expect(loadSavedDatabaseExportOptions(emptyStorage)).toEqual(DEFAULT_DATABASE_EXPORT_OPTIONS);
const corruptStorage = createMemoryStorage({
[DATABASE_EXPORT_OPTIONS_STORAGE_KEY]: "{ invalid json",
});
expect(loadSavedDatabaseExportOptions(corruptStorage)).toEqual(DEFAULT_DATABASE_EXPORT_OPTIONS);
});
it("saves and loads export options accurately", () => {
const storage = createMemoryStorage();
saveDatabaseExportOptions(
{
includeStructure: false,
includeData: true,
insertDialect: "standard",
insertMode: "single",
includeObjects: false,
includeCreateDatabase: true,
dropTableIfExists: true,
omitAutoIncrement: true,
preserveOriginalLanguage: true,
splitSqlOutput: true,
splitSqlPartMaxMb: 250,
},
storage,
);
const loaded = loadSavedDatabaseExportOptions(storage);
expect(loaded).toEqual({
includeStructure: false,
includeData: true,
insertDialect: "standard",
insertMode: "single",
includeObjects: false,
includeCreateDatabase: true,
dropTableIfExists: true,
omitAutoIncrement: true,
preserveOriginalLanguage: true,
splitSqlOutput: true,
splitSqlPartMaxMb: 250,
});
});
it("preserves unmodified fields when saving partial options", () => {
const storage = createMemoryStorage();
saveDatabaseExportOptions({ dropTableIfExists: true }, storage);
const loaded = loadSavedDatabaseExportOptions(storage);
expect(loaded.dropTableIfExists).toBe(true);
expect(loaded.includeStructure).toBe(DEFAULT_DATABASE_EXPORT_OPTIONS.includeStructure);
expect(loaded.insertMode).toBe(DEFAULT_DATABASE_EXPORT_OPTIONS.insertMode);
});
it("clamps splitSqlPartMaxMb within valid bounds", () => {
const storage = createMemoryStorage();
saveDatabaseExportOptions({ splitSqlPartMaxMb: 99999 }, storage);
expect(loadSavedDatabaseExportOptions(storage).splitSqlPartMaxMb).toBe(4096);
saveDatabaseExportOptions({ splitSqlPartMaxMb: -10 }, storage);
expect(loadSavedDatabaseExportOptions(storage).splitSqlPartMaxMb).toBe(1);
});
it("sorts table names alphabetically", () => {
const unsorted = ["zoo", "alpha", "orders", "bravo", "mango", "order_items"];
expect(sortDatabaseTableNames(unsorted)).toEqual(["alpha", "bravo", "mango", "order_items", "orders", "zoo"]);
});
});
@@ -0,0 +1,84 @@
import { DEFAULT_SQL_INSERT_DIALECT, DEFAULT_SQL_INSERT_MODE, type SqlInsertDialect, type SqlInsertMode } from "./sqlInsertMode";
export interface DatabaseExportSavedOptions {
includeStructure: boolean;
includeData: boolean;
insertDialect: SqlInsertDialect;
insertMode: SqlInsertMode;
includeObjects: boolean;
includeCreateDatabase: boolean;
dropTableIfExists: boolean;
omitAutoIncrement: boolean;
preserveOriginalLanguage: boolean;
splitSqlOutput: boolean;
splitSqlPartMaxMb: number;
}
export const MIN_SPLIT_SQL_PART_MB = 1;
export const MAX_SPLIT_SQL_PART_MB = 4096;
export const DEFAULT_DATABASE_EXPORT_OPTIONS: DatabaseExportSavedOptions = {
includeStructure: true,
includeData: true,
insertDialect: DEFAULT_SQL_INSERT_DIALECT,
insertMode: DEFAULT_SQL_INSERT_MODE,
includeObjects: true,
includeCreateDatabase: false,
dropTableIfExists: false,
omitAutoIncrement: false,
preserveOriginalLanguage: false,
splitSqlOutput: false,
splitSqlPartMaxMb: 100,
};
export const DATABASE_EXPORT_OPTIONS_STORAGE_KEY = "dbx-database-export-last-options-v1";
export function loadSavedDatabaseExportOptions(storage?: Pick<Storage, "getItem">): DatabaseExportSavedOptions {
try {
const store = storage ?? (typeof localStorage !== "undefined" ? localStorage : undefined);
if (!store) return { ...DEFAULT_DATABASE_EXPORT_OPTIONS };
const raw = store.getItem(DATABASE_EXPORT_OPTIONS_STORAGE_KEY);
if (!raw) return { ...DEFAULT_DATABASE_EXPORT_OPTIONS };
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return { ...DEFAULT_DATABASE_EXPORT_OPTIONS };
return {
includeStructure: typeof parsed.includeStructure === "boolean" ? parsed.includeStructure : DEFAULT_DATABASE_EXPORT_OPTIONS.includeStructure,
includeData: typeof parsed.includeData === "boolean" ? parsed.includeData : DEFAULT_DATABASE_EXPORT_OPTIONS.includeData,
insertDialect: parsed.insertDialect === "standard" ? "standard" : DEFAULT_DATABASE_EXPORT_OPTIONS.insertDialect,
insertMode: parsed.insertMode === "single" ? "single" : DEFAULT_DATABASE_EXPORT_OPTIONS.insertMode,
includeObjects: typeof parsed.includeObjects === "boolean" ? parsed.includeObjects : DEFAULT_DATABASE_EXPORT_OPTIONS.includeObjects,
includeCreateDatabase: typeof parsed.includeCreateDatabase === "boolean" ? parsed.includeCreateDatabase : DEFAULT_DATABASE_EXPORT_OPTIONS.includeCreateDatabase,
dropTableIfExists: typeof parsed.dropTableIfExists === "boolean" ? parsed.dropTableIfExists : DEFAULT_DATABASE_EXPORT_OPTIONS.dropTableIfExists,
omitAutoIncrement: typeof parsed.omitAutoIncrement === "boolean" ? parsed.omitAutoIncrement : DEFAULT_DATABASE_EXPORT_OPTIONS.omitAutoIncrement,
preserveOriginalLanguage: typeof parsed.preserveOriginalLanguage === "boolean" ? parsed.preserveOriginalLanguage : DEFAULT_DATABASE_EXPORT_OPTIONS.preserveOriginalLanguage,
splitSqlOutput: typeof parsed.splitSqlOutput === "boolean" ? parsed.splitSqlOutput : DEFAULT_DATABASE_EXPORT_OPTIONS.splitSqlOutput,
splitSqlPartMaxMb: typeof parsed.splitSqlPartMaxMb === "number" && Number.isFinite(parsed.splitSqlPartMaxMb) ? Math.min(MAX_SPLIT_SQL_PART_MB, Math.max(MIN_SPLIT_SQL_PART_MB, Math.round(parsed.splitSqlPartMaxMb))) : DEFAULT_DATABASE_EXPORT_OPTIONS.splitSqlPartMaxMb,
};
} catch {
return { ...DEFAULT_DATABASE_EXPORT_OPTIONS };
}
}
export function saveDatabaseExportOptions(options: Partial<DatabaseExportSavedOptions>, storage?: Pick<Storage, "setItem" | "getItem">): void {
try {
const store = storage ?? (typeof localStorage !== "undefined" ? localStorage : undefined);
if (!store) return;
const current = loadSavedDatabaseExportOptions(store);
const updated: DatabaseExportSavedOptions = {
...current,
...options,
};
store.setItem(DATABASE_EXPORT_OPTIONS_STORAGE_KEY, JSON.stringify(updated));
} catch {
// Ignore storage write failures (quota exceeded, private browsing, etc.)
}
}
export function sortDatabaseTableNames(names: string[]): string[] {
// Pin the collation so the order does not shift with the OS locale.
return [...names].sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
}