From b5b3c771b2eb18a56ce385156915885ff4caa6ed Mon Sep 17 00:00:00 2001 From: CSXFanMeng <105690322+CSXFanMeng@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:20:55 +0800 Subject: [PATCH] feat(sql-file): support ordered local SQL file includes --- .../SqlFileExecutionDialog.retry.spec.ts | 60 ++ .../sql-file/SqlFileExecutionDialog.vue | 12 + .../i18n/__tests__/featureMessages.spec.ts | 6 + apps/desktop/src/i18n/locales/az.ts | 2 + apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/id.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/ko.ts | 2 + apps/desktop/src/i18n/locales/pl.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/ru.ts | 2 + apps/desktop/src/i18n/locales/th.ts | 2 + apps/desktop/src/i18n/locales/tr.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + apps/desktop/src/lib/backend/tauri.ts | 1 + crates/dbx-core/src/data/sql_file_import.rs | 136 ++++- .../src/data/sql_file_import/includes.rs | 572 ++++++++++++++++++ crates/dbx-core/src/query/mod.rs | 1 + crates/dbx-core/tests/live_mysql57.rs | 3 + .../tests/live_mysql_database_export.rs | 3 + .../live_oceanbase_sql_file_transaction.rs | 1 + .../tests/live_oracle_database_export.rs | 1 + .../tests/live_postgres_all_schema_export.rs | 2 + ...e_postgres_export_deferred_foreign_keys.rs | 1 + .../live_postgres_export_function_order.rs | 1 + .../live_postgres_sql_file_transaction.rs | 1 + .../tests/live_sqlserver_completion.rs | 1 + .../tests/live_sqlserver_split_zip_e2e.rs | 1 + .../tests/sql_file_include_preflight.rs | 87 +++ crates/dbx-sql-core/src/sql.rs | 35 +- crates/dbx-web/src/routes/sql_file.rs | 24 + docs/content/docs/sql-file.cn.mdx | 18 + docs/content/docs/sql-file.mdx | 18 + .../issue-11043-include-source-error.png | Bin 0 -> 63597 bytes .../issue-11043-include-source-mobile.png | Bin 0 -> 38705 bytes .../issue-11043-include-source-progress.png | Bin 0 -> 60557 bytes .../issue-11043-sql-includes-desktop.png | Bin 0 -> 54710 bytes .../issue-11043-sql-includes-mobile.png | Bin 0 -> 32309 bytes docs/testing/pr-preflight.md | 2 +- 42 files changed, 1007 insertions(+), 8 deletions(-) create mode 100644 crates/dbx-core/src/data/sql_file_import/includes.rs create mode 100644 crates/dbx-core/tests/sql_file_include_preflight.rs create mode 100644 docs/screenshots/issue-11043-include-source-error.png create mode 100644 docs/screenshots/issue-11043-include-source-mobile.png create mode 100644 docs/screenshots/issue-11043-include-source-progress.png create mode 100644 docs/screenshots/issue-11043-sql-includes-desktop.png create mode 100644 docs/screenshots/issue-11043-sql-includes-mobile.png diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts index 45551e4197..7e20ed2f69 100644 --- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts +++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts @@ -322,6 +322,66 @@ afterEach(() => { root = undefined; }); +describe("SqlFileExecutionDialog includes", () => { + it("shows included source paths in live statement progress and retained failure details", async () => { + await mountReadyDialog(); + const gate = deferred(); + mocks.executeSqlFiles.mockImplementationOnce(async (request: { executionId: string }) => { + mocks.progressHandler?.(progress(request.executionId, "running", { statementIndex: 1, statementSummary: "[shared/seed.sql] INSERT INTO missing VALUES(1)" })); + await gate.promise; + mocks.progressHandler?.(progress(request.executionId, "statementFailed", { statementIndex: 1, failureCount: 1, statementSummary: "[shared/seed.sql] INSERT INTO missing VALUES(1)", error: "[shared/seed.sql] no such table: missing" })); + mocks.progressHandler?.(progress(request.executionId, "done", { statementIndex: 1, failureCount: 1 })); + }); + findButton("sqlFile.execute").click(); + try { + await vi.waitFor(() => expect(root!.textContent).toContain("[shared/seed.sql] INSERT INTO missing")); + } finally { + gate.resolve(); + } + await vi.waitFor(() => expect(root!.textContent).toContain("[shared/seed.sql] no such table: missing")); + expect(mocks.trackerTask.sqlFileFailures[0].statementSummary).toContain("[shared/seed.sql]"); + }); + + it("keeps includes opt-in and submits the checked option with the entry paths", async () => { + await mountReadyDialog(); + const label = Array.from(root!.querySelectorAll("label")).find((item) => item.textContent?.includes("sqlFile.resolveIncludes"))!; + const input = label.querySelector('input[type="checkbox"]')!; + expect(input.checked).toBe(false); + expect(input.disabled).toBe(false); + input.click(); + await nextTick(); + expect(root!.textContent).toContain("sqlFile.resolveIncludesHint"); + findButton("sqlFile.execute").click(); + await vi.waitFor(() => expect(mocks.executeSqlFiles).toHaveBeenCalledWith(expect.objectContaining({ resolveIncludes: true }), ["/tmp/first.sql", "/tmp/second.sql"])); + expect(input.disabled).toBe(true); + }); + + it("does not offer server-local includes for prepared Web uploads", async () => { + mocks.desktop = false; + root = document.createElement("div"); + document.body.append(root); + app = createApp(SqlFileExecutionDialog, { open: true, prefillPreview: { fileName: "main.sql", filePath: "/server/tmp/main.sql", sizeBytes: 9, preview: "SELECT 1;", canExecuteWithoutSelectedDatabase: true } }); + app.mount(root); + await vi.waitFor(() => expect(findButton("sqlFile.execute").disabled).toBe(false)); + expect(root.textContent).not.toContain("sqlFile.resolveIncludes"); + findButton("sqlFile.execute").click(); + await vi.waitFor(() => expect(mocks.executeSqlFiles).toHaveBeenCalledWith(expect.objectContaining({ resolveIncludes: false }), ["/server/tmp/main.sql"])); + }); + + it("disables includes for compressed SQL files", async () => { + mocks.openFileDialog.mockResolvedValue(["/tmp/backup.sql.gz"]); + root = document.createElement("div"); + document.body.append(root); + app = createApp(SqlFileExecutionDialog, { open: true }); + app.mount(root); + await vi.waitFor(() => expect(mocks.fetchSqlFileTargetOptions).toHaveBeenCalled()); + findButton("sqlFile.browse").click(); + await vi.waitFor(() => expect(mocks.previewSqlFile).toHaveBeenCalled()); + const label = Array.from(root.querySelectorAll("label")).find((item) => item.textContent?.includes("sqlFile.resolveIncludes"))!; + expect(label.querySelector("input")!.disabled).toBe(true); + }); +}); + describe("SqlFileExecutionDialog retries", () => { async function mountRestoredPreview() { mocks.desktop = false; diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue index 1506c46017..2439668d36 100644 --- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue +++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue @@ -77,6 +77,7 @@ const isDesktopRuntime = isTauriRuntime(); const fileInput = ref(null); const previews = ref([]); const encoding = ref("auto"); +const resolveIncludes = ref(false); const previewError = ref(""); let previewGeneration = 0; let selectedSources: Array = []; @@ -345,6 +346,10 @@ onBeforeUnmount(() => { }); const restoreSelectedTables = ref(false); +const canResolveIncludes = computed(() => isDesktopRuntime && !restoreSelectedTables.value && previews.value.length > 0 && previews.value.every((item) => !item.packageFilePaths && item.fileName.toLowerCase().endsWith(".sql"))); +watch(canResolveIncludes, (allowed) => { + if (!allowed) resolveIncludes.value = false; +}); const backupTables = ref([]); const selectedTableKeys = ref(new Set()); const tableSearch = ref(""); @@ -569,6 +574,7 @@ function resetState() { const initialTarget = resolveInitialTarget(); previews.value = []; encoding.value = "auto"; + resolveIncludes.value = false; selectingFile.value = false; loadingPreview.value = false; applyTarget(initialTarget); @@ -903,6 +909,7 @@ async function startExecution() { ...(target.schema ? { schema: target.schema } : {}), filePath: executionPaths[0]!, encoding: encoding.value, + resolveIncludes: canResolveIncludes.value && resolveIncludes.value, continueOnError: continueOnError.value, ...(txnSessionId.value ? { txnSessionId: txnSessionId.value } : {}), ...(restoreSelectedTables.value ? { selectedTables: selectedTables.value.map((table) => ({ ...table })) } : {}), @@ -1212,6 +1219,11 @@ watch( {{ t("toolbar.manualTransaction") }}

{{ t("sqlFile.manualTransactionHint") }}

+ +

{{ t("sqlFile.resolveIncludesHint") }}