feat(backup): support restoring selected MySQL tables
This commit is contained in:
1 parent
e8b3c1457a
commit
2d9521898f
30 files changed
+1014
-9
No files matched your search
+107
-2
@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
|
||||
listenSqlFileProgress: vi.fn(),
|
||||
openFileDialog: vi.fn(),
|
||||
previewSqlFile: vi.fn(),
|
||||
inspectSqlFileTables: vi.fn(),
|
||||
progressHandler: undefined as undefined | ((progress: Record<string, unknown>) => void),
|
||||
refreshDatabaseTreeNode: vi.fn(),
|
||||
requestConfirmation: vi.fn(),
|
||||
@@ -41,7 +42,7 @@ vi.mock("@/composables/useExportTracker", () => ({
|
||||
useExportTracker: () => ({ addSqlFileTask: mocks.addSqlFileTask, updateSqlFileTask: mocks.updateSqlFileTask }),
|
||||
}));
|
||||
vi.mock("@/composables/useDatabaseOptions", () => ({ fetchSqlFileTargetOptions: mocks.fetchSqlFileTargetOptions }));
|
||||
vi.mock("@/lib/connection/connectionLevelDatabaseBootstrap", () => ({ requiresSqlFileTargetDatabaseSelection: () => false }));
|
||||
vi.mock("@/lib/connection/connectionLevelDatabaseBootstrap", () => ({ requiresSqlFileTargetDatabaseSelection: () => false, supportsConnectionLevelDatabaseBootstrap: (connection: any) => connection?.db_type === "mysql" }));
|
||||
vi.mock("@/lib/database/productionSafety", () => ({ productionContextForDatabase: () => ({ active: false, databases: [] }) }));
|
||||
vi.mock("@/stores/productionSafetyStore", () => ({
|
||||
useProductionSafetyStore: () => ({ requestConfirmation: mocks.requestConfirmation }),
|
||||
@@ -59,6 +60,7 @@ vi.mock("@/lib/backend/api", () => ({
|
||||
executeSqlFiles: mocks.executeSqlFiles,
|
||||
listenSqlFileProgress: mocks.listenSqlFileProgress,
|
||||
previewSqlFile: mocks.previewSqlFile,
|
||||
inspectSqlFileTables: mocks.inspectSqlFileTables,
|
||||
}));
|
||||
vi.mock("@lucide/vue", () => {
|
||||
const Icon = passthrough("span");
|
||||
@@ -77,7 +79,15 @@ vi.mock("@/components/ui/tooltip", () => ({
|
||||
TooltipTrigger: passthrough("div"),
|
||||
}));
|
||||
vi.mock("@/components/ui/button", () => ({ Button: passthrough("button") }));
|
||||
vi.mock("@/components/ui/input", () => ({ Input: passthrough("input") }));
|
||||
vi.mock("@/components/ui/input", () => ({
|
||||
Input: defineComponent({
|
||||
props: ["modelValue"],
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
return () => h("input", { value: props.modelValue, onInput: (event: Event) => emit("update:modelValue", (event.target as HTMLInputElement).value) });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/components/ui/label", () => ({ Label: passthrough("label") }));
|
||||
vi.mock("@/components/ui/select", () => ({
|
||||
Select: passthrough("div"),
|
||||
@@ -181,6 +191,11 @@ beforeEach(() => {
|
||||
}
|
||||
});
|
||||
mocks.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.inspectSqlFileTables.mockResolvedValue([
|
||||
{ database: "app", name: "users" },
|
||||
{ database: "app", name: "orders" },
|
||||
{ database: "archive", name: "users" },
|
||||
]);
|
||||
mocks.fetchSqlFileTargetOptions.mockResolvedValue([]);
|
||||
mocks.openFileDialog.mockResolvedValue(["/tmp/first.sql", "/tmp/second.sql"]);
|
||||
mocks.previewSqlFile.mockImplementation(async (filePath: string) => ({
|
||||
@@ -296,3 +311,93 @@ describe("SqlFileExecutionDialog retries", () => {
|
||||
expect(root!.textContent).not.toContain("stale statement failure");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SqlFileExecutionDialog selected-table restore", () => {
|
||||
async function mountBackup() {
|
||||
root = document.createElement("div");
|
||||
document.body.append(root);
|
||||
app = createApp(SqlFileExecutionDialog, { open: true, prefillConnectionId: "mysql-1", prefillDatabase: "app", prefillFilePath: "/tmp/backup.sql.gz" });
|
||||
app.mount(root);
|
||||
await vi.waitFor(() => expect(findButton("sqlFile.execute").disabled).toBe(false));
|
||||
root.querySelectorAll<HTMLInputElement>('input[type="radio"]')[1]!.click();
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
it("scans the backup and sends only checked table identities", async () => {
|
||||
await mountBackup();
|
||||
await vi.waitFor(() => expect(root!.textContent).toContain("archive.users"));
|
||||
expect(mocks.inspectSqlFileTables).toHaveBeenCalledWith("/tmp/backup.sql.gz");
|
||||
expect(findButton("sqlFile.execute").disabled).toBe(true);
|
||||
const label = Array.from(root!.querySelectorAll("label")).find((label) => label.textContent === "app.users")!;
|
||||
label.querySelector<HTMLInputElement>("input")!.click();
|
||||
await nextTick();
|
||||
mocks.executeSqlFiles.mockImplementationOnce(async (request) => {
|
||||
mocks.progressHandler?.(progress(request.executionId, "done", { successCount: 2 }));
|
||||
});
|
||||
findButton("sqlFile.execute").click();
|
||||
await vi.waitFor(() => expect(mocks.executeSqlFiles).toHaveBeenCalledWith(expect.objectContaining({ selectedTables: [{ database: "app", name: "users" }] }), ["/tmp/backup.sql.gz"]));
|
||||
});
|
||||
|
||||
it("selects only search matches and preserves selections outside the search", async () => {
|
||||
await mountBackup();
|
||||
await vi.waitFor(() => expect(root!.textContent).toContain("archive.users"));
|
||||
const search = root!.querySelector<HTMLInputElement>('[aria-label="sqlFile.searchBackupTables"]')!;
|
||||
search.value = "app.";
|
||||
search.dispatchEvent(new Event("input"));
|
||||
await nextTick();
|
||||
root!.querySelector<HTMLInputElement>('[data-table-restore] input[type="checkbox"]')!.click();
|
||||
await nextTick();
|
||||
search.value = "archive";
|
||||
search.dispatchEvent(new Event("input"));
|
||||
await nextTick();
|
||||
expect(root!.querySelector<HTMLInputElement>('[data-table-restore] input[type="checkbox"]')!.checked).toBe(false);
|
||||
mocks.executeSqlFiles.mockImplementationOnce(async (request) => {
|
||||
mocks.progressHandler?.(progress(request.executionId, "done"));
|
||||
});
|
||||
findButton("sqlFile.execute").click();
|
||||
await vi.waitFor(() =>
|
||||
expect(mocks.executeSqlFiles).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
selectedTables: [
|
||||
{ database: "app", name: "users" },
|
||||
{ database: "app", name: "orders" },
|
||||
],
|
||||
}),
|
||||
expect.any(Array),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks execution on scan failure and allows switching back to full restore", async () => {
|
||||
mocks.inspectSqlFileTables.mockRejectedValueOnce(new Error("unsupported dump statement"));
|
||||
await mountBackup();
|
||||
await vi.waitFor(() => expect(root!.querySelector('[role="alert"]')?.textContent).toContain("unsupported dump statement"));
|
||||
expect(findButton("sqlFile.execute").disabled).toBe(true);
|
||||
root!.querySelector<HTMLInputElement>('input[type="radio"]')!.click();
|
||||
await nextTick();
|
||||
expect(findButton("sqlFile.execute").disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores a scan result after switching back to all contents", async () => {
|
||||
let finish!: (tables: any[]) => void;
|
||||
mocks.inspectSqlFileTables.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
finish = resolve;
|
||||
}),
|
||||
);
|
||||
await mountBackup();
|
||||
expect(findButton("sqlFile.execute").disabled).toBe(true);
|
||||
root!.querySelector<HTMLInputElement>('input[type="radio"]')!.click();
|
||||
await nextTick();
|
||||
finish([{ database: null, name: "stale" }]);
|
||||
await nextTick();
|
||||
expect(root!.textContent).not.toContain("stale");
|
||||
mocks.executeSqlFiles.mockImplementationOnce(async (request) => {
|
||||
mocks.progressHandler?.(progress(request.executionId, "done"));
|
||||
});
|
||||
findButton("sqlFile.execute").click();
|
||||
await vi.waitFor(() => expect(mocks.executeSqlFiles).toHaveBeenCalled());
|
||||
expect(mocks.executeSqlFiles.mock.calls[0]![0]).not.toHaveProperty("selectedTables");
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,8 @@ import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
|
||||
import { productionContextForDatabase } from "@/lib/database/productionSafety";
|
||||
import { connectionIsEffectivelyReadOnly, ensureReadOnlyWriteAccess } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { fetchSqlFileTargetOptions } from "@/composables/useDatabaseOptions";
|
||||
import { requiresSqlFileTargetDatabaseSelection } from "@/lib/connection/connectionLevelDatabaseBootstrap";
|
||||
import { cancelSqlFileExecution, executeSqlFiles, listenSqlFileProgress, previewSqlFile, type SqlFilePreview, type SqlFileProgress, type SqlFileStatus } from "@/lib/backend/api";
|
||||
import { requiresSqlFileTargetDatabaseSelection, supportsConnectionLevelDatabaseBootstrap } from "@/lib/connection/connectionLevelDatabaseBootstrap";
|
||||
import { cancelSqlFileExecution, executeSqlFiles, inspectSqlFileTables, listenSqlFileProgress, previewSqlFile, type SqlFilePreview, type SqlFileProgress, type SqlFileStatus, type SqlFileTable } from "@/lib/backend/api";
|
||||
import { buildDisplayFileNames, tooltipText as computeTooltipText } from "./sqlFilePreviewLabel";
|
||||
import { useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
@@ -135,9 +135,59 @@ const sqlConnections = computed(() => store.connections.filter((c) => !["redis",
|
||||
|
||||
const selectedConnection = computed(() => sqlConnections.value.find((c) => c.id === connectionId.value));
|
||||
|
||||
const restoreSelectedTables = ref(false);
|
||||
const backupTables = ref<SqlFileTable[]>([]);
|
||||
const selectedTableKeys = ref(new Set<string>());
|
||||
const tableSearch = ref("");
|
||||
const loadingTables = ref(false);
|
||||
const tableScanError = ref("");
|
||||
let tableScanGeneration = 0;
|
||||
const canSelectTables = computed(() => previews.value.length === 1 && supportsConnectionLevelDatabaseBootstrap(selectedConnection.value));
|
||||
const tableKey = (table: SqlFileTable) => JSON.stringify([table.database, table.name]);
|
||||
const tableLabel = (table: SqlFileTable) => (table.database ? `${table.database}.${table.name}` : table.name);
|
||||
const filteredBackupTables = computed(() => backupTables.value.filter((table) => tableLabel(table).toLocaleLowerCase().includes(tableSearch.value.trim().toLocaleLowerCase())));
|
||||
const selectedTables = computed(() => backupTables.value.filter((table) => selectedTableKeys.value.has(tableKey(table))));
|
||||
const allFilteredTablesSelected = computed(() => filteredBackupTables.value.length > 0 && filteredBackupTables.value.every((table) => selectedTableKeys.value.has(tableKey(table))));
|
||||
|
||||
function toggleTable(table: SqlFileTable) {
|
||||
const key = tableKey(table);
|
||||
if (selectedTableKeys.value.has(key)) selectedTableKeys.value.delete(key);
|
||||
else selectedTableKeys.value.add(key);
|
||||
}
|
||||
|
||||
function toggleFilteredTables() {
|
||||
const deselect = allFilteredTablesSelected.value;
|
||||
for (const table of filteredBackupTables.value) {
|
||||
if (deselect) selectedTableKeys.value.delete(tableKey(table));
|
||||
else selectedTableKeys.value.add(tableKey(table));
|
||||
}
|
||||
}
|
||||
|
||||
watch([restoreSelectedTables, canSelectTables, () => previews.value[0]?.filePath, connectionId, open], async () => {
|
||||
const generation = ++tableScanGeneration;
|
||||
backupTables.value = [];
|
||||
selectedTableKeys.value = new Set();
|
||||
tableSearch.value = "";
|
||||
tableScanError.value = "";
|
||||
loadingTables.value = false;
|
||||
if (!canSelectTables.value) restoreSelectedTables.value = false;
|
||||
if (!open.value || !restoreSelectedTables.value || !canSelectTables.value) return;
|
||||
loadingTables.value = true;
|
||||
try {
|
||||
const tables = await inspectSqlFileTables(previews.value[0]!.filePath);
|
||||
if (generation !== tableScanGeneration) return;
|
||||
backupTables.value = tables;
|
||||
} catch (error: any) {
|
||||
if (generation === tableScanGeneration) tableScanError.value = error?.message || String(error);
|
||||
} finally {
|
||||
if (generation === tableScanGeneration) loadingTables.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const canStart = computed(() => {
|
||||
const connection = selectedConnection.value;
|
||||
if (previews.value.length === 0 || !connection || running.value || loadingPreview.value || loadingDatabases.value) return false;
|
||||
if (restoreSelectedTables.value && (loadingTables.value || tableScanError.value || selectedTables.value.length === 0)) return false;
|
||||
let hasDatabaseContext = false;
|
||||
const canExecuteWithoutSelectedDatabase = previews.value.every((item) => {
|
||||
if (!hasDatabaseContext && !item.canExecuteWithoutSelectedDatabase) return false;
|
||||
@@ -259,6 +309,7 @@ function resetState() {
|
||||
databaseOptions.value = [];
|
||||
loadingDatabases.value = false;
|
||||
continueOnError.value = false;
|
||||
restoreSelectedTables.value = false;
|
||||
resetExecution();
|
||||
}
|
||||
|
||||
@@ -495,6 +546,7 @@ async function startExecution() {
|
||||
database: database.value.trim(),
|
||||
filePath: previews.value[0]!.filePath,
|
||||
continueOnError: continueOnError.value,
|
||||
...(restoreSelectedTables.value ? { selectedTables: selectedTables.value.map((table) => ({ ...table })) } : {}),
|
||||
},
|
||||
previews.value.map((item) => item.filePath),
|
||||
);
|
||||
@@ -702,6 +754,33 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canSelectTables" class="min-w-0 space-y-2.5" data-table-restore>
|
||||
<Label class="text-xs">{{ t("sqlFile.restoreScope") }}</Label>
|
||||
<div class="flex items-center gap-4 text-xs">
|
||||
<label class="flex items-center gap-2"><input v-model="restoreSelectedTables" type="radio" :value="false" :disabled="running" name="sql-file-restore-scope" />{{ t("sqlFile.restoreAll") }}</label>
|
||||
<label class="flex items-center gap-2"><input v-model="restoreSelectedTables" type="radio" :value="true" :disabled="running" name="sql-file-restore-scope" />{{ t("sqlFile.restoreSelectedTables") }}</label>
|
||||
</div>
|
||||
<template v-if="restoreSelectedTables">
|
||||
<p class="text-xs text-muted-foreground">{{ t("sqlFile.restoreTablesOnly") }}</p>
|
||||
<div v-if="loadingTables" class="flex items-center gap-2 text-xs" role="status"><Loader2 class="h-3.5 w-3.5 animate-spin" />{{ t("sqlFile.scanningTables") }}</div>
|
||||
<p v-else-if="tableScanError" class="break-words text-xs text-destructive" role="alert">{{ tableScanError }}</p>
|
||||
<template v-else>
|
||||
<Input v-model="tableSearch" :placeholder="t('sqlFile.searchBackupTables')" :aria-label="t('sqlFile.searchBackupTables')" :disabled="running" class="h-8 text-xs" />
|
||||
<div class="flex items-center justify-between gap-3 text-xs">
|
||||
<label class="flex items-center gap-2"><input type="checkbox" :checked="allFilteredTablesSelected" :disabled="running || filteredBackupTables.length === 0" @change="toggleFilteredTables" />{{ t("sqlFile.selectVisibleTables") }}</label>
|
||||
<span>{{ t("sqlFile.selectedTableCount", { selected: selectedTables.length, total: backupTables.length }) }}</span>
|
||||
</div>
|
||||
<div class="max-h-44 overflow-y-auto border rounded-md p-2 text-xs">
|
||||
<label v-for="table in filteredBackupTables" :key="tableKey(table)" class="flex min-w-0 items-center gap-2 py-1">
|
||||
<input type="checkbox" :checked="selectedTableKeys.has(tableKey(table))" :disabled="running" @change="toggleTable(table)" />
|
||||
<span class="min-w-0 break-all">{{ tableLabel(table) }}</span>
|
||||
</label>
|
||||
<p v-if="filteredBackupTables.length === 0" class="text-muted-foreground">{{ t("sqlFile.noBackupTables") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 space-y-2.5">
|
||||
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{{ t("sqlFile.options") }}
|
||||
|
||||
@@ -6167,6 +6167,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "Tezliklə",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "Bərpa əhatəsi",
|
||||
restoreAll: "Bütün məzmun",
|
||||
restoreSelectedTables: "Seçilmiş cədvəllər",
|
||||
restoreTablesOnly: "Yalnız cədvəl strukturu və məlumatları bərpa olunur. Görünüşlər, saxlanılan prosedurlar, funksiyalar, triggerlər və hadisələr daxil edilmir. Mövcud xarici açarlar və triggerlər bərpaya təsir edə bilər.",
|
||||
scanningTables: "Ehtiyat nüsxədəki cədvəllər skan edilir...",
|
||||
searchBackupTables: "Ehtiyat nüsxədə cədvəl axtar...",
|
||||
selectVisibleTables: "Görünən cədvəlləri seç",
|
||||
selectedTableCount: "{selected} / {total} cədvəl seçilib",
|
||||
noBackupTables: "Ehtiyat nüsxədə uyğun cədvəl yoxdur",
|
||||
title: "SQL faylını icra et",
|
||||
file: "Fayl",
|
||||
selectSqlFile: "SQL faylı seç",
|
||||
|
||||
@@ -6166,6 +6166,15 @@ export default {
|
||||
generateDataComingSoon: "Coming soon",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "Restore scope",
|
||||
restoreAll: "All contents",
|
||||
restoreSelectedTables: "Selected tables",
|
||||
restoreTablesOnly: "Restore table structure and data only. Views, routines, triggers and events are excluded. Existing foreign keys and triggers may affect the restore.",
|
||||
scanningTables: "Scanning backup tables...",
|
||||
searchBackupTables: "Search backup tables...",
|
||||
selectVisibleTables: "Select visible tables",
|
||||
selectedTableCount: "{selected} / {total} tables selected",
|
||||
noBackupTables: "No matching tables in the backup",
|
||||
title: "Execute SQL File",
|
||||
file: "File",
|
||||
selectSqlFile: "Select SQL file",
|
||||
|
||||
@@ -5793,6 +5793,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "Próximamente",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "Alcance de la restauración",
|
||||
restoreAll: "Todo el contenido",
|
||||
restoreSelectedTables: "Tablas seleccionadas",
|
||||
restoreTablesOnly: "Solo se restauran la estructura y los datos de las tablas. Se excluyen las vistas, los procedimientos almacenados, las funciones, los disparadores y los eventos. Las claves foráneas y los disparadores existentes pueden afectar a la restauración.",
|
||||
scanningTables: "Analizando las tablas de la copia de seguridad...",
|
||||
searchBackupTables: "Buscar tablas en la copia de seguridad...",
|
||||
selectVisibleTables: "Seleccionar las tablas visibles",
|
||||
selectedTableCount: "{selected} / {total} tablas seleccionadas",
|
||||
noBackupTables: "No hay tablas coincidentes en la copia de seguridad",
|
||||
title: "Ejecutar archivo SQL",
|
||||
file: "Archivo",
|
||||
selectSqlFile: "Seleccionar archivo SQL",
|
||||
|
||||
@@ -5791,6 +5791,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "Prossimamente",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "Ambito del ripristino",
|
||||
restoreAll: "Tutto il contenuto",
|
||||
restoreSelectedTables: "Tabelle selezionate",
|
||||
restoreTablesOnly: "Ripristina solo la struttura e i dati delle tabelle. Viste, procedure memorizzate, funzioni, trigger ed eventi sono esclusi. Le chiavi esterne e i trigger esistenti possono influire sul ripristino.",
|
||||
scanningTables: "Scansione delle tabelle nel backup...",
|
||||
searchBackupTables: "Cerca tabelle nel backup...",
|
||||
selectVisibleTables: "Seleziona le tabelle visibili",
|
||||
selectedTableCount: "{selected} / {total} tabelle selezionate",
|
||||
noBackupTables: "Nessuna tabella corrispondente nel backup",
|
||||
title: "Esegui File SQL",
|
||||
file: "File",
|
||||
selectSqlFile: "Seleziona file SQL",
|
||||
|
||||
@@ -5829,6 +5829,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "近日公開",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "復元範囲",
|
||||
restoreAll: "すべての内容",
|
||||
restoreSelectedTables: "指定したテーブル",
|
||||
restoreTablesOnly: "テーブル構造とデータのみを復元します。ビュー、ストアドプロシージャ、関数、トリガー、イベントは含まれません。既存の外部キーやトリガーが復元に影響する場合があります。",
|
||||
scanningTables: "バックアップ内のテーブルをスキャン中...",
|
||||
searchBackupTables: "バックアップ内のテーブルを検索...",
|
||||
selectVisibleTables: "表示中のテーブルを選択",
|
||||
selectedTableCount: "{total} テーブル中 {selected} テーブルを選択",
|
||||
noBackupTables: "バックアップ内に一致するテーブルがありません",
|
||||
runInBackground: "バックグラウンドで実行",
|
||||
title: "SQLファイルを実行",
|
||||
file: "ファイル",
|
||||
|
||||
@@ -5498,6 +5498,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "준비 중",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "복원 범위",
|
||||
restoreAll: "모든 내용",
|
||||
restoreSelectedTables: "선택한 테이블",
|
||||
restoreTablesOnly: "테이블 구조와 데이터만 복원합니다. 뷰, 저장 프로시저, 함수, 트리거 및 이벤트는 제외됩니다. 기존 외래 키와 트리거가 복원에 영향을 줄 수 있습니다.",
|
||||
scanningTables: "백업의 테이블을 검색하는 중...",
|
||||
searchBackupTables: "백업의 테이블 검색...",
|
||||
selectVisibleTables: "표시된 테이블 선택",
|
||||
selectedTableCount: "테이블 {selected} / {total}개 선택됨",
|
||||
noBackupTables: "백업에 일치하는 테이블이 없습니다",
|
||||
title: "SQL 파일 실행",
|
||||
file: "파일",
|
||||
selectSqlFile: "SQL 파일 선택",
|
||||
|
||||
@@ -5793,6 +5793,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "Em breve",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "Escopo da restauração",
|
||||
restoreAll: "Todo o conteúdo",
|
||||
restoreSelectedTables: "Tabelas selecionadas",
|
||||
restoreTablesOnly: "Restaura apenas a estrutura e os dados das tabelas. Views, procedimentos armazenados, funções, gatilhos e eventos não são incluídos. Chaves estrangeiras e gatilhos existentes podem afetar a restauração.",
|
||||
scanningTables: "Verificando as tabelas do backup...",
|
||||
searchBackupTables: "Pesquisar tabelas no backup...",
|
||||
selectVisibleTables: "Selecionar as tabelas visíveis",
|
||||
selectedTableCount: "{selected} / {total} tabelas selecionadas",
|
||||
noBackupTables: "Nenhuma tabela correspondente no backup",
|
||||
title: "Executar Arquivo SQL",
|
||||
file: "Arquivo",
|
||||
selectSqlFile: "Selecionar arquivo SQL",
|
||||
|
||||
@@ -6049,6 +6049,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "Yakında",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "Geri yükleme kapsamı",
|
||||
restoreAll: "Tüm içerik",
|
||||
restoreSelectedTables: "Seçili tablolar",
|
||||
restoreTablesOnly: "Yalnızca tablo yapısı ve verileri geri yüklenir. Görünümler, saklı yordamlar, işlevler, tetikleyiciler ve olaylar dahil edilmez. Mevcut yabancı anahtarlar ve tetikleyiciler geri yüklemeyi etkileyebilir.",
|
||||
scanningTables: "Yedekteki tablolar taranıyor...",
|
||||
searchBackupTables: "Yedekteki tabloları ara...",
|
||||
selectVisibleTables: "Görünür tabloları seç",
|
||||
selectedTableCount: "{selected} / {total} tablo seçildi",
|
||||
noBackupTables: "Yedekte eşleşen tablo yok",
|
||||
title: "SQL Dosyasını Çalıştır",
|
||||
file: "Dosya",
|
||||
selectSqlFile: "SQL dosyası seçin",
|
||||
|
||||
@@ -6150,6 +6150,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "即将推出",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "恢复范围",
|
||||
restoreAll: "全部内容",
|
||||
restoreSelectedTables: "指定表",
|
||||
restoreTablesOnly: "仅恢复表结构与数据,不包含视图、存储过程、函数、触发器和事件。现有外键和触发器可能影响恢复。",
|
||||
scanningTables: "正在扫描备份中的表...",
|
||||
searchBackupTables: "搜索备份中的表...",
|
||||
selectVisibleTables: "选择当前列表中的表",
|
||||
selectedTableCount: "已选择 {selected} / {total} 张表",
|
||||
noBackupTables: "备份中没有匹配的表",
|
||||
title: "执行 SQL 文件",
|
||||
file: "文件",
|
||||
selectSqlFile: "选择 SQL 文件",
|
||||
|
||||
@@ -5113,6 +5113,15 @@ export default withEnglishFallback({
|
||||
generateDataComingSoon: "即將推出",
|
||||
},
|
||||
sqlFile: {
|
||||
restoreScope: "還原範圍",
|
||||
restoreAll: "全部內容",
|
||||
restoreSelectedTables: "指定資料表",
|
||||
restoreTablesOnly: "僅還原資料表結構與資料,不包含檢視、預存程序、函式、觸發程序和事件。現有的外部索引鍵與觸發程序可能影響還原。",
|
||||
scanningTables: "正在掃描備份中的資料表...",
|
||||
searchBackupTables: "搜尋備份中的資料表...",
|
||||
selectVisibleTables: "選取目前清單中的資料表",
|
||||
selectedTableCount: "已選取 {selected} / {total} 張資料表",
|
||||
noBackupTables: "備份中沒有符合的資料表",
|
||||
title: "執行 SQL 檔案",
|
||||
file: "檔案",
|
||||
selectSqlFile: "選擇 SQL 檔案",
|
||||
|
||||
@@ -384,6 +384,7 @@ export const previewSqlFile = forward("previewSqlFile");
|
||||
export const executeSqlFile = forward("executeSqlFile");
|
||||
export const executeSqlFiles = forward("executeSqlFiles");
|
||||
export const cancelSqlFileExecution = forward("cancelSqlFileExecution");
|
||||
export const inspectSqlFileTables = forward("inspectSqlFileTables");
|
||||
export const listenSqlFileProgress = forward("listenSqlFileProgress");
|
||||
export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles");
|
||||
export const pendingOpenDbFiles = forward("pendingOpenDbFiles");
|
||||
@@ -967,6 +968,7 @@ export type {
|
||||
SqlFileStatus,
|
||||
SqlFileRequest,
|
||||
SqlFilePreview,
|
||||
SqlFileTable,
|
||||
SqlFileProgress,
|
||||
TransferRequest,
|
||||
TransferProgress,
|
||||
|
||||
@@ -121,6 +121,7 @@ import type {
|
||||
HistoryConnectionOption,
|
||||
SqlFileRequest,
|
||||
SqlFilePreview,
|
||||
SqlFileTable,
|
||||
SqlFileProgress,
|
||||
TransferRequest,
|
||||
TransferProgress,
|
||||
@@ -2252,6 +2253,10 @@ export async function executeSqlFile(request: SqlFileRequest): Promise<void> {
|
||||
return post("/api/sql-file/execute", { request });
|
||||
}
|
||||
|
||||
export async function inspectSqlFileTables(filePath: string): Promise<SqlFileTable[]> {
|
||||
return post("/api/sql-file/tables", { filePath });
|
||||
}
|
||||
|
||||
export async function executeSqlFiles(request: SqlFileRequest, filePaths: string[]): Promise<void> {
|
||||
return post("/api/sql-file/execute", { request, filePaths });
|
||||
}
|
||||
|
||||
@@ -4583,6 +4583,16 @@ export interface SqlFileRequest {
|
||||
database: string;
|
||||
filePath: string;
|
||||
continueOnError: boolean;
|
||||
selectedTables?: SqlFileTable[];
|
||||
}
|
||||
|
||||
export interface SqlFileTable {
|
||||
database: string | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function inspectSqlFileTables(filePath: string): Promise<SqlFileTable[]> {
|
||||
return invoke("inspect_sql_file_tables", { filePath });
|
||||
}
|
||||
|
||||
export interface SqlFilePreview {
|
||||
|
||||
Reference in new issue
Block a user