From 39e34d44f77edd305c75c83b4c412e152949f38e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 8 Sep 2026 19:42:32 +0800 Subject: [PATCH] feat(settings): configure SQL file size limits --- apps/desktop/src/App.vue | 10 +- .../editor/EditorSettingsDialog.vue | 122 +++++++++++++++++- .../src/components/layout/SqlFilePanel.vue | 6 +- .../src/components/layout/SqlLibraryPanel.vue | 3 +- .../sql-file/SqlFileExecutionDialog.vue | 23 +++- .../composables/useExternalSqlFileChanges.ts | 8 +- apps/desktop/src/composables/useFileDrop.ts | 8 +- apps/desktop/src/i18n/locales/en.ts | 7 +- apps/desktop/src/i18n/locales/es.ts | 7 +- apps/desktop/src/i18n/locales/it.ts | 7 +- apps/desktop/src/i18n/locales/ja.ts | 7 +- apps/desktop/src/i18n/locales/ko.ts | 7 +- apps/desktop/src/i18n/locales/pt-BR.ts | 7 +- apps/desktop/src/i18n/locales/tr.ts | 7 +- apps/desktop/src/i18n/locales/zh-CN.ts | 7 +- apps/desktop/src/i18n/locales/zh-TW.ts | 7 +- .../src/lib/__tests__/sql/sqlFileOpen.spec.ts | 46 ++++++- apps/desktop/src/lib/backend/api.ts | 2 + apps/desktop/src/lib/backend/http.ts | 17 ++- apps/desktop/src/lib/backend/tauri.ts | 16 ++- .../src/lib/settings/editorSettingsDraft.ts | 3 + .../src/lib/settings/settingsSearch.ts | 4 +- .../src/lib/settings/settingsTransfer.ts | 1 + apps/desktop/src/lib/sql/sqlFileOpen.ts | 25 +++- .../stores/__tests__/settingsStore.spec.ts | 8 ++ apps/desktop/src/stores/settingsStore.ts | 5 + crates/dbx-core/src/sql_file_import.rs | 7 + crates/dbx-core/src/storage.rs | 69 +++++++++- crates/dbx-web/src/main.rs | 9 +- crates/dbx-web/src/routes/app_settings.rs | 30 ++++- crates/dbx-web/src/routes/sql_file.rs | 54 ++++++-- .../app-tests/externalSqlFileStartup.test.ts | 2 +- .../sqlLibraryImportEncoding.test.ts | 2 +- src-tauri/src/commands/external_sql.rs | 90 +++++++++---- 34 files changed, 554 insertions(+), 79 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index ff78d004cc..9e9733022a 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -66,7 +66,7 @@ import { isMacOS, isWindows } from "@/lib/backend/platform"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { openQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile"; import { rememberExternalSqlFileTarget, resolveExternalSqlFileTarget, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget"; -import { externalSqlFileOpenErrorMessage, isSqlFilePath, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen"; +import { externalSqlFileOpenErrorMessage, externalSqlEditorMaxBytes, isSqlFilePath, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen"; import type { ConnectionConfig, DatabaseType, ObjectSourceKind, QueryTab, TreeNode } from "@/types/database"; import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink"; import { parseAiConfigDeepLink, type AiConfigDeepLinkDraft } from "@/lib/ai/aiConfigDeepLink"; @@ -2001,7 +2001,7 @@ async function openSqlFile() { if (path) { const sqlPath = path as string; openedSqlPath = sqlPath; - const snapshot = await api.readExternalSqlFileSnapshot(sqlPath); + const snapshot = await api.readExternalSqlFileSnapshot(sqlPath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb)); queryStore.updateSql(tab.id, snapshot.content); queryStore.linkExternalSqlPath(tab.id, sqlPath, sqlFileTitleFromPath(sqlPath), snapshot.version); applyExternalSqlFileTarget(tab, sqlPath); @@ -2014,7 +2014,7 @@ async function openSqlFile() { const file = input.files?.[0]; if (!file) return; try { - queryStore.updateSql(tab.id, await readBrowserSqlFile(file)); + queryStore.updateSql(tab.id, await readBrowserSqlFile(file, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb))); } catch (e: any) { toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000); } @@ -2057,7 +2057,7 @@ async function openSqlFilePath(path: string) { if (!isTauriRuntime()) return; try { await desktopOpenTabsRestorationBarrier?.settled; - const snapshot = await api.readExternalSqlFileSnapshot(path); + const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb)); const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget()); queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog, target.schema); } catch (e: any) { @@ -2703,7 +2703,7 @@ async function handleQuickOpenSelect(item: any) { // Handle SQL file types first — they don't require a database connection if (item.type === "sql_file" && item.filePath) { try { - const snapshot = await api.readExternalSqlFileSnapshot(item.filePath); + const snapshot = await api.readExternalSqlFileSnapshot(item.filePath, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb)); const target = resolveExternalSqlFileTarget(item.filePath, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget()); queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog, target.schema); } catch (e: any) { diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index e3e89ec3cd..58693204d1 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -138,6 +138,8 @@ import { saveMaxAgentTurns, loadMaxRetries, saveMaxRetries, + loadSqlFileUploadMaxBytes, + saveSqlFileUploadMaxMb, saveWebdavSyncSecretsPreference, saveWebdavSavedPassword, saveSnippetSavedToken, @@ -226,6 +228,7 @@ import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview"; import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter"; import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize"; import { MAX_QUERY_RESULT_MAX_ROWS } from "@/lib/dataGrid/queryResultRowLimit"; +import { MAX_EXTERNAL_SQL_EDITOR_FILE_MB, MIN_EXTERNAL_SQL_EDITOR_FILE_MB, clampExternalSqlEditorMaxMbInput } from "@/lib/sql/sqlFileOpen"; import type { PromptTemplate } from "@/types/promptTemplate"; import { GLOBAL_INSTRUCTIONS_MAX, PROMPT_TEMPLATE_CONTENT_MAX, PROMPT_TEMPLATE_NAME_MAX, promptTemplateCharacterCount } from "@/types/promptTemplate"; import { METADATA_CACHE_HARD_MAX_MEMORY_MB, METADATA_CACHE_MIN_MEMORY_MB, normalizeMetadataCacheMemoryMb } from "@/lib/metadata/metadataRuntimeCache"; @@ -512,6 +515,7 @@ const editPageSize = ref(settingsStore.editorSettings.pageSize); const editTableOpenPageSize = ref(settingsStore.editorSettings.tableOpenPageSize); const editQueryResultMaxRowsEnabled = ref(settingsStore.editorSettings.queryResultMaxRowsEnabled); const editQueryResultMaxRows = ref(settingsStore.editorSettings.queryResultMaxRows); +const editExternalSqlEditorMaxMb = ref(settingsStore.editorSettings.externalSqlEditorMaxMb); const editInfiniteScroll = ref(settingsStore.editorSettings.infiniteScroll); const editRegexMaxMatchCount = ref(settingsStore.editorSettings.regexMaxMatchCount); const editAutoCalculateTotalRows = ref(settingsStore.editorSettings.autoCalculateTotalRows); @@ -542,6 +546,18 @@ function updateQueryResultMaxRowsInput(event: Event) { editQueryResultMaxRows.value = normalized; } +function updateExternalSqlEditorMaxMbInput(event: Event) { + const input = event.currentTarget as HTMLInputElement; + const parsed = Number(input.value); + if (!Number.isFinite(parsed) || parsed > MAX_EXTERNAL_SQL_EDITOR_FILE_MB) { + input.value = String(editExternalSqlEditorMaxMb.value); + return; + } + const normalized = clampExternalSqlEditorMaxMbInput(input.value); + if (input.value !== String(normalized)) input.value = String(normalized); + editExternalSqlEditorMaxMb.value = normalized; +} + function sqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles): boolean { return editSqlVariableSyntaxOverrides.value[editSqlVariableSyntaxDatabaseType.value]?.[key] ?? true; } @@ -725,6 +741,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { tableOpenPageSize: editTableOpenPageSize.value, queryResultMaxRowsEnabled: editQueryResultMaxRowsEnabled.value, queryResultMaxRows: editQueryResultMaxRows.value, + externalSqlEditorMaxMb: editExternalSqlEditorMaxMb.value, infiniteScroll: editInfiniteScroll.value, regexMaxMatchCount: editRegexMaxMatchCount.value, autoCalculateTotalRows: editAutoCalculateTotalRows.value, @@ -1221,6 +1238,7 @@ function syncEditorSettingsDraftFromStore() { editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize; editQueryResultMaxRowsEnabled.value = settingsStore.editorSettings.queryResultMaxRowsEnabled; editQueryResultMaxRows.value = settingsStore.editorSettings.queryResultMaxRows; + editExternalSqlEditorMaxMb.value = settingsStore.editorSettings.externalSqlEditorMaxMb; editInfiniteScroll.value = settingsStore.editorSettings.infiniteScroll; editRegexMaxMatchCount.value = settingsStore.editorSettings.regexMaxMatchCount; editAutoCalculateTotalRows.value = settingsStore.editorSettings.autoCalculateTotalRows; @@ -1441,6 +1459,9 @@ function applyEditorSettingsKeysToRefs(draft: EditorSettingsDraft, keys: readonl case "queryResultMaxRows": editQueryResultMaxRows.value = draft.queryResultMaxRows; break; + case "externalSqlEditorMaxMb": + editExternalSqlEditorMaxMb.value = draft.externalSqlEditorMaxMb; + break; case "infiniteScroll": editInfiniteScroll.value = draft.infiniteScroll; break; @@ -1900,6 +1921,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize; editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled; editQueryResultMaxRows.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRows; + editExternalSqlEditorMaxMb.value = DEFAULT_EDITOR_SETTINGS.externalSqlEditorMaxMb; editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll; editRegexMaxMatchCount.value = DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount; editAutoCalculateTotalRows.value = DEFAULT_EDITOR_SETTINGS.autoCalculateTotalRows; @@ -1987,6 +2009,7 @@ function resetAllDefaults() { editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize; editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled; editQueryResultMaxRows.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRows; + editExternalSqlEditorMaxMb.value = DEFAULT_EDITOR_SETTINGS.externalSqlEditorMaxMb; editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll; editRegexMaxMatchCount.value = DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount; editAutoCalculateTotalRows.value = DEFAULT_EDITOR_SETTINGS.autoCalculateTotalRows; @@ -2368,6 +2391,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[ { value: "formatter", label: t("settings.sqlFormatterTab") }, { value: "navigation", label: t("settings.navigationTab") }, { value: "data", label: t("settings.dataTab") }, + { value: "sqlFile", label: t("settings.sqlFileSizeTab") }, ...(isWeb ? [] : [{ value: "backups" as const, label: t("databaseBackup.title") }]), { value: "tunnels", label: t("settings.tunnelsTab") }, { value: "shortcuts", label: t("settings.shortcutsTab") }, @@ -2378,7 +2402,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[ ...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []), { value: "about", label: t("settings.aboutTab") }, ]); -const settingsTabsWithApplyFooter = new Set(["editor", "formatter", "appearance", "navigation", "data", "shortcuts", "snippets"]); +const settingsTabsWithApplyFooter = new Set(["editor", "formatter", "appearance", "navigation", "data", "sqlFile", "shortcuts", "snippets"]); function hasSettingsApplyFooter(value: SettingsCategory): boolean { return settingsTabsWithApplyFooter.has(value); @@ -3826,6 +3850,7 @@ watch(activeSettingsTab, async (tab) => { await promptTemplateStore.ensureLoaded(); editGlobalInstructions.value = promptTemplateStore.globalInstructions; } + if (tab === "sqlFile" && isWeb) void loadWebSqlFileUploadMaxMbSetting(); if (tab === "about" && !appSupportInfo.value) void refreshAppSupportInfo(); if (tab === "appearance") { checkLayoutDescTruncation(); @@ -4133,6 +4158,52 @@ function normalizeMaxRetries(value: number | undefined): number { return Math.min(10, Math.max(0, rounded)); } +const editWebSqlFileUploadMaxMb = ref(undefined); +const webSqlFileUploadMaxMbSaving = ref(false); +const webSqlFileUploadMaxMbLoaded = ref(false); +const webSqlFileUploadMaxMbLoading = ref(false); +const webSqlFileUploadMaxMbLoadError = ref(""); + +async function loadWebSqlFileUploadMaxMbSetting() { + if (webSqlFileUploadMaxMbLoaded.value || webSqlFileUploadMaxMbLoading.value) return; + webSqlFileUploadMaxMbLoading.value = true; + webSqlFileUploadMaxMbLoadError.value = ""; + try { + const bytes = await loadSqlFileUploadMaxBytes(); + editWebSqlFileUploadMaxMb.value = Math.round(bytes / (1024 * 1024)); + webSqlFileUploadMaxMbLoaded.value = true; + } catch (e: any) { + webSqlFileUploadMaxMbLoadError.value = e?.message || String(e); + toast(webSqlFileUploadMaxMbLoadError.value, 5000); + } finally { + webSqlFileUploadMaxMbLoading.value = false; + } +} + +function normalizeWebSqlFileUploadMaxMb(value: number | undefined): number { + const rounded = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 200; + return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, rounded)); +} + +function webSqlFileUploadMaxMbOutOfRange(value: number | undefined): boolean { + return typeof value === "number" && (value < MIN_EXTERNAL_SQL_EDITOR_FILE_MB || value > MAX_EXTERNAL_SQL_EDITOR_FILE_MB); +} + +async function saveWebSqlFileUploadMaxMbSetting() { + if (!webSqlFileUploadMaxMbLoaded.value) return; + const clamped = normalizeWebSqlFileUploadMaxMb(editWebSqlFileUploadMaxMb.value); + webSqlFileUploadMaxMbSaving.value = true; + try { + await saveSqlFileUploadMaxMb(clamped); + editWebSqlFileUploadMaxMb.value = clamped; + toast(t("settings.sqlFileUploadMaxMbSaved")); + } catch (e: any) { + toast(e?.message || String(e), 5000); + } finally { + webSqlFileUploadMaxMbSaving.value = false; + } +} + // AI Config Delete Confirmation const aiDeleteConfirmOpen = ref(false); const aiDeleteConfigId = ref(null); @@ -7427,6 +7498,55 @@ onUnmounted(() => { +
+
+
+ +

+ {{ t("settings.externalSqlEditorMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }} +

+
+ +
+
+
+ +

+ {{ t("settings.webSqlFileUploadMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }} +

+
+
+ + +
+
+
+
diff --git a/apps/desktop/src/components/layout/SqlFilePanel.vue b/apps/desktop/src/components/layout/SqlFilePanel.vue index 9a912f4c29..30cac3a03b 100644 --- a/apps/desktop/src/components/layout/SqlFilePanel.vue +++ b/apps/desktop/src/components/layout/SqlFilePanel.vue @@ -10,12 +10,13 @@ import LightTooltip from "@/components/ui/LightTooltip.vue"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { useQueryStore } from "@/stores/queryStore"; import { useConnectionStore } from "@/stores/connectionStore"; +import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { translateBackendError } from "@/i18n/backend-errors"; import { copyToClipboard } from "@/lib/common/clipboard"; import { forgetExternalSqlFileTarget, moveExternalSqlFileTarget, resolveExternalSqlFileTarget, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget"; -import { externalSqlFileOpenErrorMessage, formatSqlFileSize, isExternalSqlFileTooLargeError, isSqlFilePath } from "@/lib/sql/sqlFileOpen"; +import { externalSqlFileOpenErrorMessage, externalSqlEditorMaxBytes, formatSqlFileSize, isExternalSqlFileTooLargeError, isSqlFilePath } from "@/lib/sql/sqlFileOpen"; import * as api from "@/lib/backend/api"; import type { SqlFileEntry } from "@/lib/backend/api"; import { getSqlFileFilter, getSqlFileFolderPaths, saveSqlFileFilter, saveSqlFileFolderPaths, notifySqlFileFoldersChanged } from "@/lib/sqlFile/sqlFileFolders"; @@ -28,6 +29,7 @@ const emit = defineEmits<{ const { t } = useI18n(); const queryStore = useQueryStore(); const connectionStore = useConnectionStore(); +const settingsStore = useSettingsStore(); const { toast } = useToast(); interface FolderState { @@ -253,7 +255,7 @@ function collectDirPaths(entries: SqlFileEntry[], into: Set) { async function openFile(path: string) { if (!isTauriRuntime()) return; try { - const snapshot = await api.readExternalSqlFileSnapshot(path); + const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb)); const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget()); queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog, target.schema); } catch (e: any) { diff --git a/apps/desktop/src/components/layout/SqlLibraryPanel.vue b/apps/desktop/src/components/layout/SqlLibraryPanel.vue index cf8913a8f2..afe14291c7 100644 --- a/apps/desktop/src/components/layout/SqlLibraryPanel.vue +++ b/apps/desktop/src/components/layout/SqlLibraryPanel.vue @@ -16,6 +16,7 @@ import { useSavedSqlStore } from "@/stores/savedSqlStore"; import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; +import { externalSqlEditorMaxBytes } from "@/lib/sql/sqlFileOpen"; import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus"; import { savedSqlFolderBranchFileCount } from "@/lib/savedSql/savedSqlFolderCounts"; import { collectSavedSqlDirectoryImportFiles } from "@/lib/savedSql/savedSqlDirectoryImport"; @@ -231,7 +232,7 @@ async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) { takenNamesByFolder.set(folderKey, takenNames); } const path = file.path; - const content = await api.readExternalSqlFile(path); + const content = await api.readExternalSqlFile(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb)); const displayName = uniqueImportedName(file.name, takenNames); await savedSqlStore.saveFile({ connectionId: importTarget.connectionId, diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue index e909477bfe..9a2f52e4da 100644 --- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue +++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue @@ -1,5 +1,5 @@