feat(settings): configure SQL file size limits

This commit is contained in:
t8y2 authored and GitHub committed 2026-09-08 19:42:32 +08:00
1 parent 9a0e3e744c
commit 39e34d44f7
34 files changed
+554 -79

No files matched your search

@@ -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<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "shortcuts", "snippets"]);
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["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<number | undefined>(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<string | null>(null);
@@ -7427,6 +7498,55 @@ onUnmounted(() => {
</div>
</section>
<section v-else-if="activeSettingsTab === 'sqlFile'" data-settings-search-id="sqlFile" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('sqlFile')]">
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="external-sql-editor-max-mb">
{{ t("settings.externalSqlEditorMaxMb") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.externalSqlEditorMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }}
</p>
</div>
<Input
id="external-sql-editor-max-mb"
type="number"
inputmode="numeric"
class="h-7 w-[130px] px-2 text-left text-xs tabular-nums"
:min="MIN_EXTERNAL_SQL_EDITOR_FILE_MB"
:max="MAX_EXTERNAL_SQL_EDITOR_FILE_MB"
:model-value="editExternalSqlEditorMaxMb"
@input="updateExternalSqlEditorMaxMbInput"
/>
</div>
<div v-if="isWeb" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="web-sql-file-upload-max-mb">
{{ t("settings.webSqlFileUploadMaxMb") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.webSqlFileUploadMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }}
</p>
</div>
<div class="flex shrink-0 items-center gap-2">
<Input
id="web-sql-file-upload-max-mb"
v-model.number="editWebSqlFileUploadMaxMb"
type="number"
inputmode="numeric"
class="h-7 w-[130px] px-2 text-left text-xs tabular-nums"
:min="MIN_EXTERNAL_SQL_EDITOR_FILE_MB"
:max="MAX_EXTERNAL_SQL_EDITOR_FILE_MB"
:disabled="!webSqlFileUploadMaxMbLoaded || webSqlFileUploadMaxMbLoading"
:aria-invalid="webSqlFileUploadMaxMbOutOfRange(editWebSqlFileUploadMaxMb)"
/>
<Button type="button" size="sm" variant="outline" :disabled="!webSqlFileUploadMaxMbLoaded || webSqlFileUploadMaxMbSaving || webSqlFileUploadMaxMbOutOfRange(editWebSqlFileUploadMaxMb)" @click="saveWebSqlFileUploadMaxMbSetting">
{{ t("settings.save") }}
</Button>
</div>
</div>
</section>
<section v-else-if="activeSettingsTab === 'shortcuts'" data-settings-search-id="shortcuts" :class="['flex flex-col gap-2 py-2', settingsSearchTargetClass('shortcuts')]">
<div class="relative">
<Search class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -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<string>) {
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) {
@@ -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,
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { uuid } from "@/lib/common/utils";
import { useI18n } from "vue-i18n";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
@@ -97,7 +97,21 @@ const terminalError = ref("");
const activeExecutionTask = ref<ExportTask | null>(null);
const failureDetailsExpanded = ref(false);
const refreshedTarget = ref(false);
const MAX_WEB_SQL_FILE_BYTES = 200 * 1024 * 1024;
const DEFAULT_WEB_SQL_FILE_BYTES = 200 * 1024 * 1024;
const webSqlFileUploadMaxBytes = ref(DEFAULT_WEB_SQL_FILE_BYTES);
// The web server caps SQL file uploads via DBX_SQL_FILE_UPLOAD_MAX_MB; fetch the
// effective limit once so the client-side pre-check matches the server rule.
onMounted(async () => {
if (isDesktopRuntime) return;
try {
const { loadSqlFileUploadMaxBytes } = await import("@/lib/backend/http");
const value = await loadSqlFileUploadMaxBytes();
if (Number.isFinite(value) && value > 0) webSqlFileUploadMaxBytes.value = value;
} catch {
// Fall back to the built-in default; the server still rejects oversized uploads.
}
});
// Per-file results accumulated from backend file-boundary events during
// multi-file execution. Populated only when previews.length > 1.
@@ -285,8 +299,9 @@ async function previewSelectedSqlFile(fileOrPath: string | File) {
return previewSqlFile(fileOrPath as string);
}
const file = fileOrPath as File;
if (file.size > MAX_WEB_SQL_FILE_BYTES) {
throw new Error(`File too large: ${file.size} bytes (max ${MAX_WEB_SQL_FILE_BYTES} bytes)`);
const uploadLimit = webSqlFileUploadMaxBytes.value;
if (file.size > uploadLimit) {
throw new Error(`File too large: ${file.size} bytes (max ${uploadLimit} bytes)`);
}
const { previewSqlFile: previewWebSqlFile } = await import("@/lib/backend/http");
return previewWebSqlFile(file);