feat(export): remember last export options and sort tables by name
This commit is contained in:
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" }));
|
||||
}
|
||||
Reference in new issue
Block a user