feat(settings): configure SQL file size limits
This commit is contained in:
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);
|
||||
|
||||
Reference in new issue
Block a user