fix(import): pick existing tables from database scope

This commit is contained in:
Eve authored and GitHub committed 2026-09-02 16:52:48 +08:00
1 parent 820de91972
commit e3911a0198
10 files changed
+464 -16

No files matched your search

@@ -7,6 +7,7 @@ import { Dialog, DialogHeader, DialogTitle, DialogFooter, DialogScrollContent }
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AlertTriangle, ArrowLeft, ArrowRight, Check, CheckCircle2, FileCode, FileJson, FileSpreadsheet, FileText, FileUp, Loader2, RefreshCw, Square, Upload, X } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
@@ -28,7 +29,7 @@ import {
type TableImportWizardStep,
} from "@/lib/table/tableImport";
import { getDataTypeOptions } from "@/lib/table/tableStructureEditorState";
import { tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { metadataSchemaForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import type { ColumnInfo } from "@/types/database";
import * as api from "@/lib/backend/api";
@@ -64,6 +65,9 @@ interface BatchImportTask {
const SKIP_VALUE = "__skip__";
const targetColumns = ref<ColumnInfo[]>([]);
const loadedTargetTableName = ref("");
const existingTableNames = ref<string[]>([]);
const selectedExistingTable = ref(props.prefillTable || "");
const targetMode = ref<ImportTargetMode>(props.prefillTable ? "existing" : "create");
const newTableName = ref("");
const selectedSource = ref<string | File | null>(null);
@@ -75,6 +79,7 @@ const columnMapping = ref<Record<string, string>>({});
const columnDataTypes = ref<Record<string, string>>({});
const dynamicDataTypeOptions = ref<string[]>([]);
const loadingDataTypeOptions = ref(false);
const loadingExistingTables = ref(false);
const loadingTarget = ref(false);
const loadingPreview = ref(false);
const importMode = ref<api.TableImportMode>("append");
@@ -101,6 +106,8 @@ let previewReloadTimer: ReturnType<typeof setTimeout> | null = null;
let importElapsedTimer: ReturnType<typeof setInterval> | null = null;
let importStartedAt = 0;
let dataTypeOptionsRequestId = 0;
let existingTablesRequestId = 0;
let targetColumnsRequestId = 0;
let previewRequestId = 0;
let batchEncodingRequestId = 0;
@@ -131,9 +138,11 @@ const wizardSteps: Array<{ value: TableImportWizardStep; labelKey: string }> = [
const selectedConnection = computed(() => (props.prefillConnectionId ? store.getConfig(props.prefillConnectionId) : undefined));
const structureDatabaseType = computed(() => tableStructureDatabaseTypeForConnection(selectedConnection.value));
const targetSchema = computed(() => metadataSchemaForConnection(selectedConnection.value, props.prefillDatabase || "", props.prefillSchema));
const dataTypeOptions = computed(() => mergeDataTypeOptions(dynamicDataTypeOptions.value, getDataTypeOptions(structureDatabaseType.value), Object.values(columnDataTypes.value)));
const hasExistingTarget = computed(() => !!props.prefillTable);
const targetTableName = computed(() => (targetMode.value === "create" ? newTableName.value.trim() : props.prefillTable || ""));
const hasExistingTarget = computed(() => !!props.prefillTable || loadingExistingTables.value || existingTableNames.value.length > 0);
const targetTableName = computed(() => (targetMode.value === "create" ? newTableName.value.trim() : selectedExistingTable.value));
const existingTargetMetadataReady = computed(() => targetMode.value !== "existing" || (!loadingTarget.value && !!selectedExistingTable.value && loadedTargetTableName.value === selectedExistingTable.value));
const targetColumnNames = computed(() => targetColumns.value.map((column) => column.name));
const mappedColumns = computed<api.TableImportColumnMapping[]>(() => {
const currentPreview = preview.value;
@@ -159,7 +168,7 @@ const requiredUnmappedColumns = computed(() =>
);
const isBatchImport = computed(() => targetMode.value === "create" && batchTasks.value.length > 1);
const canImport = computed(() => {
if (running.value || !props.prefillConnectionId) return false;
if (running.value || !props.prefillConnectionId || !existingTargetMetadataReady.value) return false;
if (!isBatchImport.value) return !!preview.value && !!targetTableName.value && mappingValidation.value.valid;
const tableNames = batchTasks.value.map((task) => task.tableName.trim().toLowerCase());
if (new Set(tableNames).size !== tableNames.length) return false;
@@ -171,8 +180,8 @@ const canImport = computed(() => {
const canGoBack = computed(() => wizardStep.value !== "source" && wizardStep.value !== "execution" && !running.value);
const canGoNext = computed(() => {
if (wizardStep.value === "source") return !!selectedSource.value && !!sourceFormat.value;
if (wizardStep.value === "options") return !!preview.value && !!targetTableName.value;
if (wizardStep.value === "mapping") return mappingValidation.value.valid;
if (wizardStep.value === "options") return !!preview.value && !!targetTableName.value && existingTargetMetadataReady.value;
if (wizardStep.value === "mapping") return existingTargetMetadataReady.value && mappingValidation.value.valid;
return false;
});
const rawProgressPercent = computed(() => tableImportProgressPercent(progress.value));
@@ -236,11 +245,16 @@ function resetState() {
liveElapsedMs.value = 0;
previewRequestId++;
batchEncodingRequestId++;
existingTablesRequestId++;
targetColumnsRequestId++;
if (previewReloadTimer) {
clearTimeout(previewReloadTimer);
previewReloadTimer = null;
}
targetColumns.value = [];
loadedTargetTableName.value = "";
existingTableNames.value = [];
selectedExistingTable.value = props.prefillTable || "";
targetMode.value = props.prefillTable ? "existing" : "create";
newTableName.value = "";
selectedSource.value = null;
@@ -263,6 +277,8 @@ function resetState() {
importMode.value = "append";
batchSize.value = 500;
loadingPreview.value = false;
loadingExistingTables.value = false;
loadingTarget.value = false;
running.value = false;
cancelling.value = false;
importId.value = "";
@@ -465,18 +481,58 @@ async function loadDataTypeOptions() {
}
}
async function loadExistingTables() {
if (props.prefillTable || loadingExistingTables.value || existingTableNames.value.length || !props.prefillConnectionId || !props.prefillDatabase) return;
const requestId = ++existingTablesRequestId;
loadingExistingTables.value = true;
errorMessage.value = "";
try {
await store.ensureConnected(props.prefillConnectionId);
const tables = await api.listTables(props.prefillConnectionId, props.prefillDatabase, targetSchema.value, undefined, undefined, undefined, ["TABLE"]);
if (requestId !== existingTablesRequestId) return;
existingTableNames.value = tables.map((table) => table.name);
} catch (e: any) {
if (requestId === existingTablesRequestId) {
existingTableNames.value = [];
errorMessage.value = String(e?.message || e);
}
} finally {
if (requestId === existingTablesRequestId) loadingExistingTables.value = false;
}
}
async function loadTargetColumns() {
if (targetMode.value !== "existing" || !props.prefillConnectionId || !props.prefillDatabase || !props.prefillTable) return;
const tableName = selectedExistingTable.value;
if (targetMode.value !== "existing" || !props.prefillConnectionId || !props.prefillDatabase || !tableName) {
targetColumnsRequestId++;
targetColumns.value = [];
loadedTargetTableName.value = "";
columnMapping.value = {};
loadingTarget.value = false;
return;
}
const requestId = ++targetColumnsRequestId;
targetColumns.value = [];
loadedTargetTableName.value = "";
columnMapping.value = {};
loadingTarget.value = true;
errorMessage.value = "";
try {
await store.ensureConnected(props.prefillConnectionId);
targetColumns.value = await api.getColumns(props.prefillConnectionId, props.prefillDatabase, props.prefillSchema || props.prefillDatabase, props.prefillTable);
const columns = await api.getColumns(props.prefillConnectionId, props.prefillDatabase, targetSchema.value, tableName);
if (requestId !== targetColumnsRequestId) return;
targetColumns.value = columns;
loadedTargetTableName.value = tableName;
applyAutoMapping();
} catch (e: any) {
errorMessage.value = String(e?.message || e);
if (requestId === targetColumnsRequestId) {
targetColumns.value = [];
loadedTargetTableName.value = "";
columnMapping.value = {};
errorMessage.value = String(e?.message || e);
}
} finally {
loadingTarget.value = false;
if (requestId === targetColumnsRequestId) loadingTarget.value = false;
}
}
@@ -671,8 +727,8 @@ function canOpenStep(step: TableImportWizardStep) {
if (running.value || step === "execution") return false;
if (step === "source") return true;
if (step === "options") return !!selectedSource.value;
if (step === "mapping") return !!preview.value;
if (step === "review") return !!preview.value && mappingValidation.value.valid;
if (step === "mapping") return !!preview.value && existingTargetMetadataReady.value;
if (step === "review") return !!preview.value && existingTargetMetadataReady.value && mappingValidation.value.valid;
return false;
}
@@ -749,7 +805,7 @@ async function startImport() {
importId: importId.value,
connectionId: props.prefillConnectionId,
database: props.prefillDatabase || "",
schema: props.prefillSchema || "",
schema: targetSchema.value,
table: tableName,
filePath: currentPreview.filePath,
sourceRef: currentPreview.sourceRef || null,
@@ -835,7 +891,7 @@ async function startBatchImport() {
importId: importId.value,
connectionId: props.prefillConnectionId,
database: props.prefillDatabase || "",
schema: props.prefillSchema || "",
schema: targetSchema.value,
table: task.tableName,
filePath: task.preview.filePath,
sourceRef: task.preview.sourceRef || null,
@@ -992,6 +1048,7 @@ watch(textEncoding, schedulePreviewReloadAfterEncodingChange);
watch([newTableName, columnMapping, columnDataTypes], saveActiveBatchTask, { deep: true });
watch(wizardStep, (step) => {
if (step !== "mapping") closeDataTypePicker();
if (step === "options") void loadExistingTables();
});
watch(targetMode, (mode) => {
if (mode === "existing") {
@@ -999,13 +1056,19 @@ watch(targetMode, (mode) => {
dynamicDataTypeOptions.value = [];
void loadTargetColumns();
} else {
targetColumnsRequestId++;
targetColumns.value = [];
loadedTargetTableName.value = "";
loadingTarget.value = false;
importMode.value = "append";
applyAutoMapping();
applySuggestedColumnDataTypes();
void loadDataTypeOptions();
}
});
watch(selectedExistingTable, () => {
if (targetMode.value === "existing") void loadTargetColumns();
});
watch(rawProgressPercent, (percent) => {
if (progress.value?.status === "running") {
@@ -1144,7 +1207,7 @@ watch(rawProgressPercent, (percent) => {
<div class="grid grid-cols-2 gap-2">
<button type="button" class="rounded-md border px-3 py-2 text-left text-xs" :class="targetMode === 'existing' ? 'border-primary bg-primary/5' : 'hover:bg-muted/30'" :disabled="!hasExistingTarget" @click="targetMode = 'existing'">
<div class="font-medium">{{ t("tableImport.existingTable") }}</div>
<div class="mt-1 truncate text-[11px] text-muted-foreground">{{ props.prefillTable || t("tableImport.noExistingTarget") }}</div>
<div class="mt-1 truncate text-[11px] text-muted-foreground">{{ selectedExistingTable || t("tableImport.noExistingTarget") }}</div>
</button>
<button type="button" class="rounded-md border px-3 py-2 text-left text-xs" :class="targetMode === 'create' ? 'border-primary bg-primary/5' : 'hover:bg-muted/30'" @click="targetMode = 'create'">
<div class="font-medium">{{ t("tableImport.createTable") }}</div>
@@ -1155,9 +1218,22 @@ watch(rawProgressPercent, (percent) => {
<div class="space-y-1.5">
<Label class="text-xs">{{ t("tableImport.targetTableName") }}</Label>
<Input v-if="targetMode === 'create'" v-model="newTableName" class="h-8 text-xs font-mono" />
<div v-else class="flex h-8 items-center rounded-md border px-2 text-xs">
<div v-else-if="props.prefillTable" class="flex h-8 items-center rounded-md border px-2 text-xs">
<span class="truncate">{{ props.prefillTable }}</span>
</div>
<SearchableSelect
v-else
:model-value="selectedExistingTable"
:options="existingTableNames"
:placeholder="t('tableImport.noExistingTarget')"
:search-placeholder="t('tableImport.searchExistingTables')"
:empty-text="t('tableImport.noTables')"
:loading-text="t('tableImport.loadingTables')"
:loading="loadingExistingTables"
:disabled="loadingExistingTables || !existingTableNames.length"
trigger-class="h-8 font-mono text-xs"
@update:model-value="(value) => (selectedExistingTable = value)"
/>
</div>
</div>
@@ -0,0 +1,348 @@
// @vitest-environment happy-dom
import { createApp, defineComponent, h, nextTick, type App } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
const mocks = vi.hoisted(() => ({
ensureConnected: vi.fn().mockResolvedValue(undefined),
listTables: vi.fn().mockResolvedValue([
{ name: "existing_target", table_type: "TABLE" },
{ name: "archived_target", table_type: "TABLE" },
]),
getColumns: vi.fn().mockResolvedValue([
{ name: "id", data_type: "INTEGER", nullable: false },
{ name: "name", data_type: "TEXT", nullable: true },
]),
listDataTypes: vi.fn().mockResolvedValue(["INTEGER", "TEXT"]),
previewTableImportFile: vi.fn().mockResolvedValue({
fileName: "rows.xlsx",
filePath: "/tmp/rows.xlsx",
fileType: "excel",
sizeBytes: 24,
columns: ["id", "name"],
rows: [[1, "Alice"]],
totalRows: 1,
sourceFingerprint: "rows-xlsx",
sheets: ["Data"],
}),
releaseTableImportSource: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
getConfig: (id: string) => {
if (id === "connection-1") return { id, name: "SQLite", db_type: "sqlite" };
if (id === "postgres-1") return { id, name: "PostgreSQL", db_type: "postgres" };
return undefined;
},
ensureConnected: mocks.ensureConnected,
invalidateMetadataCache: vi.fn(),
refreshObjectListTreeNode: vi.fn().mockResolvedValue(undefined),
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: {} }),
}));
vi.mock("@/lib/backend/api", () => ({
listTables: mocks.listTables,
getColumns: mocks.getColumns,
listDataTypes: mocks.listDataTypes,
previewTableImportFile: mocks.previewTableImportFile,
releaseTableImportSource: mocks.releaseTableImportSource,
importTableFile: vi.fn(),
cancelTableImport: vi.fn(),
}));
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
vi.mock("@/components/ui/dialog", async () => {
const { defineComponent, h } = await import("vue");
const passthrough = defineComponent({
setup(_props, { attrs, slots }) {
return () => h("div", attrs, slots.default?.());
},
});
return {
Dialog: passthrough,
DialogHeader: passthrough,
DialogTitle: passthrough,
DialogFooter: passthrough,
DialogScrollContent: passthrough,
};
});
vi.mock("@/components/ui/button", async () => {
const { defineComponent, h } = await import("vue");
return {
Button: defineComponent({
props: { disabled: Boolean },
emits: ["click"],
setup(props, { attrs, slots, emit }) {
return () => h("button", { ...attrs, disabled: props.disabled, onClick: () => emit("click") }, slots.default?.());
},
}),
};
});
vi.mock("@/components/ui/input", async () => {
const { defineComponent, h } = await import("vue");
return {
Input: defineComponent({
props: { modelValue: [String, Number], disabled: Boolean },
emits: ["update:modelValue"],
setup(props, { attrs, emit }) {
return () =>
h("input", {
...attrs,
value: props.modelValue,
disabled: props.disabled,
onInput: (event: Event) => emit("update:modelValue", (event.target as HTMLInputElement).value),
});
},
}),
};
});
vi.mock("@/components/ui/label", async () => {
const { defineComponent, h } = await import("vue");
return {
Label: defineComponent({
setup(_props, { slots }) {
return () => h("label", slots.default?.());
},
}),
};
});
vi.mock("@/components/ui/select", async () => {
const { defineComponent, h } = await import("vue");
const passthrough = defineComponent({
setup(_props, { slots }) {
return () => h("div", slots.default?.());
},
});
return {
Select: passthrough,
SelectContent: passthrough,
SelectItem: passthrough,
SelectTrigger: passthrough,
SelectValue: passthrough,
};
});
vi.mock("@/components/ui/searchable-select", async () => {
const { defineComponent, h } = await import("vue");
return {
SearchableSelect: defineComponent({
props: {
modelValue: { type: String, default: "" },
options: { type: Array<string>, default: () => [] },
disabled: Boolean,
},
emits: ["update:modelValue"],
setup(props, { emit }) {
return () =>
h(
"select",
{
class: "existing-table-select-stub",
value: props.modelValue,
disabled: props.disabled,
onChange: (event: Event) => emit("update:modelValue", (event.target as HTMLSelectElement).value),
},
[h("option", { value: "" }), ...(props.options as string[]).map((option) => h("option", { value: option }, option))],
);
},
}),
};
});
import TableImportDialog from "@/components/import/TableImportDialog.vue";
const mountedApps: App[] = [];
async function flushAsyncUpdates() {
for (let index = 0; index < 8; index += 1) {
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
async function mountDialog(
options: {
prefillTable?: string;
connectionId?: string;
database?: string;
schema?: string;
} = {},
) {
const container = document.createElement("div");
document.body.append(container);
const app = createApp(
defineComponent({
setup: () => () =>
h(TableImportDialog, {
open: true,
prefillConnectionId: options.connectionId || "connection-1",
prefillDatabase: options.database || "main",
prefillSchema: options.schema,
prefillTable: options.prefillTable,
}),
}),
);
mountedApps.push(app);
app.use(i18n);
app.mount(container);
await flushAsyncUpdates();
}
async function selectWorkbook() {
const fileInput = document.body.querySelector<HTMLInputElement>('input[type="file"]');
expect(fileInput).toBeTruthy();
Object.defineProperty(fileInput, "files", {
configurable: true,
value: [new File(["test workbook"], "rows.xlsx", { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" })],
});
fileInput?.dispatchEvent(new Event("change", { bubbles: true }));
await vi.waitFor(() => expect(mocks.previewTableImportFile).toHaveBeenCalled());
}
async function selectExistingTarget(tableName: string) {
const tableSelect = document.body.querySelector<HTMLSelectElement>("select.existing-table-select-stub");
expect(tableSelect).toBeTruthy();
if (tableSelect) {
tableSelect.value = tableName;
tableSelect.dispatchEvent(new Event("change", { bubbles: true }));
}
await nextTick();
}
function buttonContaining(text: string) {
return [...document.body.querySelectorAll<HTMLButtonElement>("button")].find((button) => button.textContent?.includes(text));
}
beforeEach(() => {
mocks.ensureConnected.mockReset().mockResolvedValue(undefined);
mocks.listTables.mockReset().mockResolvedValue([
{ name: "existing_target", table_type: "TABLE" },
{ name: "archived_target", table_type: "TABLE" },
]);
mocks.getColumns.mockReset().mockResolvedValue([
{ name: "id", data_type: "INTEGER", nullable: false },
{ name: "name", data_type: "TEXT", nullable: true },
]);
mocks.listDataTypes.mockClear();
mocks.previewTableImportFile.mockClear();
mocks.releaseTableImportSource.mockClear();
});
afterEach(() => {
for (const app of mountedApps.splice(0)) app.unmount();
document.body.textContent = "";
vi.clearAllMocks();
});
describe("TableImportDialog existing targets", () => {
it("lets a database-level import choose an existing table and loads its columns", async () => {
i18n.global.locale.value = "en";
await mountDialog();
expect(mocks.listTables).not.toHaveBeenCalled();
await selectWorkbook();
await vi.waitFor(() => {
expect(mocks.listTables).toHaveBeenCalledWith("connection-1", "main", "main", undefined, undefined, undefined, ["TABLE"]);
});
const existingTableButton = buttonContaining("Existing table");
expect(existingTableButton).toBeTruthy();
expect(existingTableButton?.disabled).toBe(false);
existingTableButton?.click();
await flushAsyncUpdates();
await selectExistingTarget("existing_target");
await vi.waitFor(() => {
expect(mocks.getColumns).toHaveBeenCalledWith("connection-1", "main", "main", "existing_target");
});
buttonContaining("Next")?.click();
await flushAsyncUpdates();
const mappings = [...document.body.querySelectorAll<HTMLSelectElement>("select")];
expect(mappings.map((select) => select.value)).toEqual(["id", "name"]);
expect(mappings.map((select) => [...select.options].map((option) => option.value))).toEqual([
["__skip__", "id", "name"],
["__skip__", "id", "name"],
]);
});
it("waits for the selected table metadata and ignores an older table response", async () => {
let resolveExisting!: (columns: Array<Record<string, unknown>>) => void;
let resolveArchived!: (columns: Array<Record<string, unknown>>) => void;
const existingColumns = new Promise<Array<Record<string, unknown>>>((resolve) => (resolveExisting = resolve));
const archivedColumns = new Promise<Array<Record<string, unknown>>>((resolve) => (resolveArchived = resolve));
mocks.getColumns.mockImplementation((_connectionId, _database, _schema, tableName) => (tableName === "existing_target" ? existingColumns : archivedColumns));
i18n.global.locale.value = "en";
await mountDialog({ schema: "main" });
await selectWorkbook();
await vi.waitFor(() => expect(mocks.listTables).toHaveBeenCalled());
buttonContaining("Existing table")?.click();
await flushAsyncUpdates();
await selectExistingTarget("existing_target");
await vi.waitFor(() => expect(mocks.getColumns).toHaveBeenCalledWith("connection-1", "main", "main", "existing_target"));
expect(buttonContaining("Next")?.disabled).toBe(true);
await selectExistingTarget("archived_target");
await vi.waitFor(() => expect(mocks.getColumns).toHaveBeenCalledWith("connection-1", "main", "main", "archived_target"));
resolveArchived([{ name: "name", data_type: "TEXT", nullable: true }]);
await flushAsyncUpdates();
expect(buttonContaining("Next")?.disabled).toBe(false);
resolveExisting([{ name: "id", data_type: "INTEGER", nullable: false }]);
await flushAsyncUpdates();
buttonContaining("Next")?.click();
await flushAsyncUpdates();
const mappings = [...document.body.querySelectorAll<HTMLSelectElement>("select")];
expect(mappings.map((select) => select.value)).toEqual(["__skip__", "name"]);
expect(mappings.every((select) => [...select.options].every((option) => option.value !== "id"))).toBe(true);
});
it("keeps navigation disabled when loading existing-table columns fails", async () => {
mocks.getColumns.mockRejectedValueOnce(new Error("metadata unavailable"));
i18n.global.locale.value = "en";
await mountDialog({ schema: "main" });
await selectWorkbook();
await vi.waitFor(() => expect(mocks.listTables).toHaveBeenCalled());
buttonContaining("Existing table")?.click();
await flushAsyncUpdates();
await selectExistingTarget("existing_target");
await vi.waitFor(() => expect(document.body.textContent).toContain("metadata unavailable"));
expect(buttonContaining("Next")?.disabled).toBe(true);
expect(buttonContaining("Mapping")?.disabled).toBe(true);
});
it("uses the connection-aware default schema for a database-level import", async () => {
i18n.global.locale.value = "en";
await mountDialog({ connectionId: "postgres-1", database: "dbx_test" });
await selectWorkbook();
await vi.waitFor(() => {
expect(mocks.listTables).toHaveBeenCalledWith("postgres-1", "dbx_test", "", undefined, undefined, undefined, ["TABLE"]);
});
});
it("keeps a table-level import prefilled without loading the table list", async () => {
i18n.global.locale.value = "en";
await mountDialog({ prefillTable: "existing_target", schema: "main" });
expect(mocks.listTables).not.toHaveBeenCalled();
expect(mocks.getColumns).toHaveBeenCalledWith("connection-1", "main", "main", "existing_target");
});
});