feat(backup): support restoring selected MySQL tables

This commit is contained in:
CSXFanMeng authored and GitHub committed 2026-09-11 23:20:05 +08:00
1 parent e8b3c1457a
commit 2d9521898f
30 files changed
+1014 -9

No files matched your search

@@ -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") }}
+9
View File
@@ -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ç",
+9
View File
@@ -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",
+9
View 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",
+9
View File
@@ -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",
+9
View File
@@ -5829,6 +5829,15 @@ export default withEnglishFallback({
generateDataComingSoon: "近日公開",
},
sqlFile: {
restoreScope: "復元範囲",
restoreAll: "すべての内容",
restoreSelectedTables: "指定したテーブル",
restoreTablesOnly: "テーブル構造とデータのみを復元します。ビュー、ストアドプロシージャ、関数、トリガー、イベントは含まれません。既存の外部キーやトリガーが復元に影響する場合があります。",
scanningTables: "バックアップ内のテーブルをスキャン中...",
searchBackupTables: "バックアップ内のテーブルを検索...",
selectVisibleTables: "表示中のテーブルを選択",
selectedTableCount: "{total} テーブル中 {selected} テーブルを選択",
noBackupTables: "バックアップ内に一致するテーブルがありません",
runInBackground: "バックグラウンドで実行",
title: "SQLファイルを実行",
file: "ファイル",
+9
View 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 파일 선택",
+9
View File
@@ -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",
+9
View File
@@ -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",
+9
View File
@@ -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 文件",
+9
View File
@@ -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 檔案",
+2
View File
@@ -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,
+5
View File
@@ -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 });
}
+10
View File
@@ -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 {