mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:04:25 +00:00
feat(settings): configure SQL file size limits
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as api from "@/lib/backend/api";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { externalSqlFileContentMatchesBaseline, externalSqlFileMetadataMatches, externalSqlFileVersionWasIgnored } from "@/lib/sql/externalSqlFileChanges";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { externalSqlEditorMaxBytes } from "@/lib/sql/sqlFileOpen";
|
||||
import type { ExternalSqlFileSnapshot } from "@/lib/backend/tauri";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
|
||||
@@ -64,6 +66,7 @@ function delay(ms: number) {
|
||||
export function useExternalSqlFileChanges(options: UseExternalSqlFileChangesOptions) {
|
||||
const { t } = useI18n();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const pendingPrompt = ref<ExternalSqlFilePrompt | null>(null);
|
||||
const checkingTabIds = new Set<string>();
|
||||
let promptResolver: ((decision: ExternalSqlFilePromptDecision) => void) | null = null;
|
||||
@@ -83,12 +86,13 @@ export function useExternalSqlFileChanges(options: UseExternalSqlFileChangesOpti
|
||||
}
|
||||
|
||||
async function readStableSnapshot(path: string) {
|
||||
const maxSizeBytes = externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb);
|
||||
try {
|
||||
return await api.readExternalSqlFileSnapshot(path);
|
||||
return await api.readExternalSqlFileSnapshot(path, maxSizeBytes);
|
||||
} catch (firstError) {
|
||||
await delay(MISSING_FILE_RECHECK_DELAY_MS);
|
||||
try {
|
||||
return await api.readExternalSqlFileSnapshot(path);
|
||||
return await api.readExternalSqlFileSnapshot(path, maxSizeBytes);
|
||||
} catch {
|
||||
throw firstError;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { uuid } from "@/lib/common/utils";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useLargeSqlFileStreamingFallback } from "@/composables/useLargeSqlFileFallback";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { ConnectionConfig, ExternalSqlFileVersion } from "@/types/database";
|
||||
import { detectDatabaseFileType } from "@/lib/database/databaseFileDetection";
|
||||
import { externalSqlFileOpenErrorMessage, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
|
||||
import { externalSqlEditorMaxBytes, externalSqlFileOpenErrorMessage, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
|
||||
import { resolveExternalSqlFileTarget, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
|
||||
|
||||
function isSqlFilePath(path: string): boolean {
|
||||
@@ -23,6 +24,7 @@ export function useFileDrop() {
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const { openInStreamingExecutorOnTooLarge } = useLargeSqlFileStreamingFallback();
|
||||
|
||||
@@ -87,7 +89,7 @@ export function useFileDrop() {
|
||||
|
||||
if (isSqlFilePath(path)) {
|
||||
try {
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path);
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb));
|
||||
await openDroppedSqlFile(name, snapshot.content, path, snapshot.version);
|
||||
} catch (e: any) {
|
||||
if (!openInStreamingExecutorOnTooLarge(path, e)) {
|
||||
@@ -128,7 +130,7 @@ export function useFileDrop() {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (!isSqlFilePath(file.name)) continue;
|
||||
void readBrowserSqlFile(file)
|
||||
void readBrowserSqlFile(file, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb))
|
||||
.then((content) => openDroppedSqlFile(file.name, content))
|
||||
.catch((e: any) => {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
|
||||
@@ -6540,6 +6540,7 @@ export default {
|
||||
appearanceTab: "Appearance",
|
||||
navigationTab: "Navigation",
|
||||
dataTab: "Data",
|
||||
sqlFileSizeTab: "SQL File Size",
|
||||
tunnelsTab: "Tunnels",
|
||||
tunnelsDescription: "Reusable SSH / proxy / HTTP tunnel configurations. Configure once, then select the profile from a connection's tunnel tab; edits here apply to every connection using the profile.",
|
||||
tunnelsEmpty: "No tunnel profiles yet. Create one here, then select it in a connection's tunnel settings.",
|
||||
@@ -6741,7 +6742,11 @@ export default {
|
||||
queryResultMaxRowsEnabled: "Limit query result rows",
|
||||
queryResultMaxRowsDescription: "Caps rows returned by queries, table data, stored procedures, and continuous loading.",
|
||||
queryResultMaxRowsTooSmall: "Must be at least the query page size ({pageSize}).",
|
||||
queryResultMaxRowsUnlimitedDescription: "No total row or continuous loading limit. Large results can use substantial memory.",
|
||||
externalSqlEditorMaxMb: "Maximum SQL editor file size",
|
||||
externalSqlEditorMaxMbDescription: "SQL files larger than this are routed to the streaming Execute SQL File dialog instead of the editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Maximum web SQL file upload size",
|
||||
webSqlFileUploadMaxMbDescription: "Largest SQL file the web server accepts for streaming execution ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL file upload size saved.",
|
||||
showColumnCommentsInHeader: "Show column comments under names",
|
||||
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
|
||||
showColumnTypesInHeader: "Show column types under names",
|
||||
|
||||
@@ -6131,6 +6131,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Apariencia",
|
||||
navigationTab: "Navegación",
|
||||
dataTab: "Datos",
|
||||
sqlFileSizeTab: "Tamaño de archivo SQL",
|
||||
tunnelsTab: "Túneles",
|
||||
tunnelsDescription: "Configuraciones reutilizables de túneles SSH / proxy / HTTP. Configura una vez y selecciona el perfil desde la pestaña de túnel de una conexión; los cambios aquí se aplican a todas las conexiones que usan el perfil.",
|
||||
tunnelsEmpty: "Aún no hay perfiles de túnel. Crea uno aquí y selecciónalo en la configuración de túnel de una conexión.",
|
||||
@@ -6321,7 +6322,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "Limitar filas del resultado",
|
||||
queryResultMaxRowsDescription: "Limita las filas conservadas por consultas, datos de tablas, procedimientos almacenados y carga continua.",
|
||||
queryResultMaxRowsTooSmall: "Debe ser al menos el tamaño de página de consulta ({pageSize}).",
|
||||
queryResultMaxRowsUnlimitedDescription: "Sin límite total de filas ni de carga continua. Los resultados grandes pueden usar mucha memoria.",
|
||||
externalSqlEditorMaxMb: "Tamaño máximo de archivo del editor SQL",
|
||||
externalSqlEditorMaxMbDescription: "Los archivos SQL mayores que esto se envían al diálogo de ejecución por streaming en lugar del editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Tamaño máximo de carga de archivo SQL web",
|
||||
webSqlFileUploadMaxMbDescription: "Archivo SQL más grande que el servidor web acepta para ejecución en streaming ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Tamaño de carga de archivo SQL web guardado.",
|
||||
showColumnCommentsInHeader: "Mostrar comentarios bajo los nombres",
|
||||
showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres",
|
||||
|
||||
@@ -6131,6 +6131,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Aspetto",
|
||||
navigationTab: "Navigazione",
|
||||
dataTab: "Dati",
|
||||
sqlFileSizeTab: "Dimensione file SQL",
|
||||
tunnelsTab: "Tunnel",
|
||||
tunnelsDescription: "Configurazioni riutilizzabili di tunnel SSH / proxy / HTTP. Configura una volta, poi seleziona il profilo dalla scheda tunnel di una connessione; le modifiche qui si applicano a tutte le connessioni che usano il profilo.",
|
||||
tunnelsEmpty: "Nessun profilo tunnel. Creane uno qui, poi selezionalo nelle impostazioni tunnel di una connessione.",
|
||||
@@ -6321,7 +6322,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "Limita le righe del risultato",
|
||||
queryResultMaxRowsDescription: "Limita le righe conservate da query, dati tabella, stored procedure e caricamento continuo.",
|
||||
queryResultMaxRowsTooSmall: "Deve essere almeno pari alle righe per pagina della query ({pageSize}).",
|
||||
queryResultMaxRowsUnlimitedDescription: "Nessun limite totale di righe o di caricamento continuo. I risultati grandi possono usare molta memoria.",
|
||||
externalSqlEditorMaxMb: "Dimensione massima file dell'editor SQL",
|
||||
externalSqlEditorMaxMbDescription: "I file SQL più grandi vengono inviati alla finestra di esecuzione in streaming invece che all'editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Dimensione massima di caricamento file SQL web",
|
||||
webSqlFileUploadMaxMbDescription: "File SQL più grande che il server web accetta per l'esecuzione in streaming ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Dimensione di caricamento file SQL web salvata.",
|
||||
showColumnCommentsInHeader: "Mostra i commenti delle colonne sotto i nomi",
|
||||
showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.",
|
||||
showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi",
|
||||
|
||||
@@ -6169,6 +6169,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "外観",
|
||||
navigationTab: "ナビゲーション",
|
||||
dataTab: "データ",
|
||||
sqlFileSizeTab: "SQLファイルサイズ",
|
||||
tunnelsTab: "トンネル",
|
||||
tunnelsDescription: "再利用可能な SSH / プロキシ / HTTP トンネル設定です。一度設定すれば、接続のトンネルタブからプロファイルを選択するだけで使えます。ここでの編集は、このプロファイルを使用するすべての接続に反映されます。",
|
||||
tunnelsEmpty: "トンネルプロファイルがまだありません。ここで作成し、接続のトンネル設定で選択してください。",
|
||||
@@ -6355,7 +6356,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "クエリ結果の行数を制限",
|
||||
queryResultMaxRowsDescription: "クエリ、テーブルデータ、ストアドプロシージャ、および連続読み込みで保持する合計行数を制限します。",
|
||||
queryResultMaxRowsTooSmall: "クエリのページ行数({pageSize})以上にしてください。",
|
||||
queryResultMaxRowsUnlimitedDescription: "合計行数または連続読み込み数を制限しません。大きな結果は大量のメモリを使用する可能性があります。",
|
||||
externalSqlEditorMaxMb: "SQLエディターの最大ファイルサイズ",
|
||||
externalSqlEditorMaxMbDescription: "このサイズを超えるSQLファイルはエディターではなくストリーミング実行ダイアログに回されます({min}~{max} MB)。",
|
||||
webSqlFileUploadMaxMb: "Web SQLファイルの最大アップロードサイズ",
|
||||
webSqlFileUploadMaxMbDescription: "Webサーバーがストリーミング実行に受け付ける最大のSQLファイルサイズ({min}~{max} MB)。",
|
||||
sqlFileUploadMaxMbSaved: "Web SQLファイルのアップロードサイズを保存しました。",
|
||||
showColumnCommentsInHeader: "列名の下にコメントを表示",
|
||||
showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。",
|
||||
showColumnTypesInHeader: "列名の下にデータ型を表示",
|
||||
|
||||
@@ -5866,6 +5866,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "모양",
|
||||
navigationTab: "탐색",
|
||||
dataTab: "데이터",
|
||||
sqlFileSizeTab: "SQL 파일 크기",
|
||||
tunnelsTab: "터널",
|
||||
tunnelsDescription: "재사용 가능한 SSH/프록시/HTTP 터널 설정입니다. 한 번 구성한 후 연결의 터널 탭에서 프로필을 선택하세요. 여기서의 편집은 이 프로필을 사용하는 모든 연결에 적용됩니다.",
|
||||
tunnelsEmpty: "아직 터널 프로필이 없습니다. 여기서 만든 후 연결의 터널 설정에서 선택하세요.",
|
||||
@@ -6054,7 +6055,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "쿼리 결과 행 수 제한",
|
||||
queryResultMaxRowsDescription: "쿼리, 테이블 데이터, 저장 프로시저 및 연속 로딩에서 유지할 총 행 수를 제한합니다.",
|
||||
queryResultMaxRowsTooSmall: "쿼리 페이지 크기({pageSize}) 이상이어야 합니다.",
|
||||
queryResultMaxRowsUnlimitedDescription: "총 행 수 또는 연속 로딩 수를 제한하지 않습니다. 큰 결과는 많은 메모리를 사용할 수 있습니다.",
|
||||
externalSqlEditorMaxMb: "SQL 편집기 최대 파일 크기",
|
||||
externalSqlEditorMaxMbDescription: "이 크기를 초과하는 SQL 파일은 편집기 대신 스트리밍 실행 대화상자로 전달됩니다({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "웹 SQL 파일 최대 업로드 크기",
|
||||
webSqlFileUploadMaxMbDescription: "웹 서버가 스트리밍 실행에 허용하는 가장 큰 SQL 파일({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "웹 SQL 파일 업로드 크기가 저장되었습니다.",
|
||||
showColumnCommentsInHeader: "이름 아래에 컬럼 주석 표시",
|
||||
showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.",
|
||||
showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시",
|
||||
|
||||
@@ -6133,6 +6133,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Aparência",
|
||||
navigationTab: "Navegação",
|
||||
dataTab: "Dados",
|
||||
sqlFileSizeTab: "Tamanho de arquivo SQL",
|
||||
tunnelsTab: "Túneis",
|
||||
tunnelsDescription: "Configurações reutilizáveis de túneis SSH / proxy / HTTP. Configure uma vez e selecione o perfil na aba de túnel de uma conexão; as edições aqui se aplicam a todas as conexões que usam o perfil.",
|
||||
tunnelsEmpty: "Ainda não há perfis de túnel. Crie um aqui e selecione-o nas configurações de túnel de uma conexão.",
|
||||
@@ -6323,7 +6324,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "Limitar linhas do resultado",
|
||||
queryResultMaxRowsDescription: "Limita as linhas mantidas por consultas, dados de tabelas, procedimentos armazenados e carregamento contínuo.",
|
||||
queryResultMaxRowsTooSmall: "Deve ser pelo menos o tamanho da página da consulta ({pageSize}).",
|
||||
queryResultMaxRowsUnlimitedDescription: "Sem limite total de linhas ou de carregamento contínuo. Resultados grandes podem usar muita memória.",
|
||||
externalSqlEditorMaxMb: "Tamanho máximo de arquivo do editor SQL",
|
||||
externalSqlEditorMaxMbDescription: "Arquivos SQL maiores são enviados para o diálogo de execução em streaming em vez do editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Tamanho máximo de upload de arquivo SQL web",
|
||||
webSqlFileUploadMaxMbDescription: "Maior arquivo SQL que o servidor web aceita para execução em streaming ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Tamanho de upload de arquivo SQL web salvo.",
|
||||
showColumnCommentsInHeader: "Mostrar comentários de coluna sob os nomes",
|
||||
showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes",
|
||||
|
||||
@@ -6426,6 +6426,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Görünüm",
|
||||
navigationTab: "Gezinme",
|
||||
dataTab: "Veri",
|
||||
sqlFileSizeTab: "SQL Dosya Boyutu",
|
||||
tunnelsTab: "Tüneller",
|
||||
tunnelsDescription: "Yeniden kullanılabilir SSH / proxy / HTTP tünel yapılandırmaları. Bir kez yapılandırın, sonra bağlantının tünel sekmesinden profili seçin; buradaki değişiklikler profili kullanan her bağlantıda geçerli olur.",
|
||||
tunnelsEmpty: "Henüz tünel profili yok. Burada bir tane oluşturun, ardından bir bağlantının tünel ayarlarından seçin.",
|
||||
@@ -6625,7 +6626,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "Sorgu sonucu satırlarını sınırla",
|
||||
queryResultMaxRowsDescription: "Sorgular, tablo verisi, saklı yordamlar ve sürekli yükleme tarafından döndürülen satırlara üst sınır koyar.",
|
||||
queryResultMaxRowsTooSmall: "En az sorgu sayfa boyutu ({pageSize}) kadar olmalıdır.",
|
||||
queryResultMaxRowsUnlimitedDescription: "Toplam satır veya sürekli yükleme sınırı yok. Büyük sonuçlar çok fazla bellek kullanabilir.",
|
||||
externalSqlEditorMaxMb: "SQL düzenleyici maksimum dosya boyutu",
|
||||
externalSqlEditorMaxMbDescription: "Bu boyuttan büyük SQL dosyaları düzenleyici yerine akış yürütme iletişim kutusuna yönlendirilir ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Web SQL dosyası maksimum yükleme boyutu",
|
||||
webSqlFileUploadMaxMbDescription: "Web sunucusunun akış yürütme için kabul ettiği en büyük SQL dosyası ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL dosyası yükleme boyutu kaydedildi.",
|
||||
showColumnCommentsInHeader: "Sütun açıklamalarını adların altında göster",
|
||||
showColumnCommentsInHeaderDescription: "Tablo sütun açıklamalarını doğrudan ızgara sütun adlarının altında göster.",
|
||||
showColumnTypesInHeader: "Sütun türlerini adların altında göster",
|
||||
|
||||
@@ -6523,6 +6523,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "外观",
|
||||
navigationTab: "导航",
|
||||
dataTab: "数据",
|
||||
sqlFileSizeTab: "SQL 文件大小",
|
||||
tunnelsTab: "隧道维护",
|
||||
tunnelsDescription: "可复用的 SSH / 代理 / HTTP 隧道配置。一次配置,之后在连接的隧道页签中选择档案即可;此处的修改会对所有使用该档案的连接生效。",
|
||||
tunnelsEmpty: "还没有隧道档案。先在这里创建,然后在连接的隧道设置中选择使用。",
|
||||
@@ -6724,7 +6725,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "限制查询结果行数",
|
||||
queryResultMaxRowsDescription: "限制查询、表数据、存储过程及连续滚动累计保留的总行数。",
|
||||
queryResultMaxRowsTooSmall: "不能小于查询结果默认每页行数({pageSize})。",
|
||||
queryResultMaxRowsUnlimitedDescription: "不限制返回总行数或连续滚动累计数量,大结果集可能占用较多内存。",
|
||||
externalSqlEditorMaxMb: "SQL 编辑器最大文件大小",
|
||||
externalSqlEditorMaxMbDescription: "超过此大小的 SQL 文件将转交流式执行 SQL 文件对话框而不是编辑器({min}–{max} MB)。",
|
||||
webSqlFileUploadMaxMb: "Web SQL 文件最大上传大小",
|
||||
webSqlFileUploadMaxMbDescription: "Web 服务器接受用于流式执行的最大 SQL 文件({min}–{max} MB)。",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL 文件上传大小已保存。",
|
||||
showColumnCommentsInHeader: "在字段名下方显示注释",
|
||||
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
|
||||
showColumnTypesInHeader: "在字段名下方显示数据类型",
|
||||
|
||||
@@ -5459,6 +5459,7 @@ export default withEnglishFallback({
|
||||
securityTab: "安全",
|
||||
aboutTab: "關於我們",
|
||||
dataTab: "資料",
|
||||
sqlFileSizeTab: "SQL 檔案大小",
|
||||
tunnelsTab: "隧道維護",
|
||||
tunnelsDescription: "可重複使用的 SSH / 代理 / HTTP 隧道設定。一次設定,之後在連線的隧道頁籤中選擇設定檔即可;此處的變更會套用到所有使用該設定檔的連線。",
|
||||
tunnelsEmpty: "還沒有隧道設定檔。先在這裡建立,然後在連線的隧道設定中選擇使用。",
|
||||
@@ -5636,7 +5637,11 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsEnabled: "限制查詢結果列數",
|
||||
queryResultMaxRowsDescription: "限制查詢、資料表資料、預存程序及連續捲動累計保留的總列數。",
|
||||
queryResultMaxRowsTooSmall: "不能小於查詢結果預設每頁列數({pageSize})。",
|
||||
queryResultMaxRowsUnlimitedDescription: "不限制傳回總列數或連續捲動累計數量,大型結果集可能占用大量記憶體。",
|
||||
externalSqlEditorMaxMb: "SQL 編輯器最大檔案大小",
|
||||
externalSqlEditorMaxMbDescription: "超過此大小的 SQL 檔案將轉交流式執行 SQL 檔案對話方塊而不是編輯器({min}–{max} MB)。",
|
||||
webSqlFileUploadMaxMb: "Web SQL 檔案最大上傳大小",
|
||||
webSqlFileUploadMaxMbDescription: "Web 伺服器接受用於串流執行之最大 SQL 檔案({min}–{max} MB)。",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL 檔案上傳大小已儲存。",
|
||||
showColumnCommentsInHeader: "在欄位名稱下方顯示註解",
|
||||
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
|
||||
showColumnTypesInHeader: "在欄位名稱下方顯示資料類型",
|
||||
|
||||
+45
-1
@@ -1,7 +1,21 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ExternalSqlFileTooLargeError, externalSqlFileDisplayTitles, externalSqlFileOpenErrorMessage, formatSqlFileSize, isSqlFilePath, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES, normalizeExternalSqlPath, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
|
||||
import {
|
||||
ExternalSqlFileTooLargeError,
|
||||
clampExternalSqlEditorMaxMbInput,
|
||||
externalSqlEditorMaxBytes,
|
||||
externalSqlFileDisplayTitles,
|
||||
externalSqlFileOpenErrorMessage,
|
||||
formatSqlFileSize,
|
||||
isSqlFilePath,
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_MB,
|
||||
MIN_EXTERNAL_SQL_EDITOR_FILE_MB,
|
||||
normalizeExternalSqlEditorMaxMb,
|
||||
normalizeExternalSqlPath,
|
||||
readBrowserSqlFile,
|
||||
} from "@/lib/sql/sqlFileOpen";
|
||||
|
||||
describe("external SQL file paths", () => {
|
||||
it("normalizes Windows separators for identity checks", () => {
|
||||
@@ -41,6 +55,36 @@ describe("external SQL file editor limit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("honors a custom browser editor limit", async () => {
|
||||
const file = new Blob(["select 1;"]);
|
||||
Object.defineProperty(file, "size", { value: 8 });
|
||||
|
||||
await expect(readBrowserSqlFile(file, 4)).rejects.toMatchObject({
|
||||
name: "ExternalSqlFileTooLargeError",
|
||||
sizeBytes: 8,
|
||||
maxSizeBytes: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes the configurable editor size limit in MB", () => {
|
||||
expect(normalizeExternalSqlEditorMaxMb(undefined)).toBe(64);
|
||||
expect(normalizeExternalSqlEditorMaxMb(0)).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(normalizeExternalSqlEditorMaxMb(128)).toBe(128);
|
||||
expect(normalizeExternalSqlEditorMaxMb(99999)).toBe(MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(normalizeExternalSqlEditorMaxMb(12.6)).toBe(13);
|
||||
});
|
||||
|
||||
it("clamps the editor size limit input", () => {
|
||||
expect(clampExternalSqlEditorMaxMbInput("abc")).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(clampExternalSqlEditorMaxMbInput(0)).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(clampExternalSqlEditorMaxMbInput(256)).toBe(256);
|
||||
expect(clampExternalSqlEditorMaxMbInput(99999)).toBe(MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
});
|
||||
|
||||
it("converts the editor size limit to bytes", () => {
|
||||
expect(externalSqlEditorMaxBytes(64)).toBe(64 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it("formats large file sizes", () => {
|
||||
expect(formatSqlFileSize(64 * 1024 * 1024)).toBe("64.0 MB");
|
||||
expect(formatSqlFileSize(50 * 1024 * 1024 * 1024)).toBe("50.0 GB");
|
||||
|
||||
@@ -313,6 +313,8 @@ export const saveDesktopSettings = forward("saveDesktopSettings");
|
||||
export const loadMcpGlobalPolicy = forward("loadMcpGlobalPolicy");
|
||||
export const saveMcpGlobalPolicy = forward("saveMcpGlobalPolicy");
|
||||
export const loadMaxAgentTurns = forward("loadMaxAgentTurns");
|
||||
export const loadSqlFileUploadMaxBytes = forward("loadSqlFileUploadMaxBytes");
|
||||
export const saveSqlFileUploadMaxMb = forward("saveSqlFileUploadMaxMb");
|
||||
export const saveMaxAgentTurns = forward("saveMaxAgentTurns");
|
||||
export const loadMaxRetries = forward("loadMaxRetries");
|
||||
export const saveMaxRetries = forward("saveMaxRetries");
|
||||
|
||||
@@ -1848,6 +1848,19 @@ export async function loadMaxAgentTurns(): Promise<number> {
|
||||
return get("/api/app-settings/max-agent-turns");
|
||||
}
|
||||
|
||||
export async function loadSqlFileUploadMaxBytes(): Promise<number> {
|
||||
return get("/api/app-settings/sql-file-upload-max-bytes");
|
||||
}
|
||||
|
||||
export async function saveSqlFileUploadMaxMb(sqlFileUploadMaxMb: number): Promise<void> {
|
||||
const res = await fetch(apiUrl("/api/app-settings/sql-file-upload-max-bytes"), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sqlFileUploadMaxMb }),
|
||||
});
|
||||
if (!res.ok) throw await backendResponseError(res);
|
||||
}
|
||||
|
||||
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
|
||||
const res = await fetch(apiUrl("/api/app-settings/max-agent-turns"), {
|
||||
method: "PUT",
|
||||
@@ -2270,11 +2283,11 @@ export async function pendingOpenAiConfigLinks(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(_path: string): Promise<string> {
|
||||
export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number): Promise<string> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
export async function readExternalSqlFileSnapshot(_path: string): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
|
||||
export async function readExternalSqlFileSnapshot(_path: string, _maxSizeBytes?: number): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
|
||||
@@ -717,6 +717,14 @@ export async function loadMaxAgentTurns(): Promise<number> {
|
||||
return invoke("load_max_agent_turns");
|
||||
}
|
||||
|
||||
export async function loadSqlFileUploadMaxBytes(): Promise<number> {
|
||||
return 200 * 1024 * 1024;
|
||||
}
|
||||
|
||||
export async function saveSqlFileUploadMaxMb(_sqlFileUploadMaxMb: number): Promise<void> {
|
||||
// No-op on desktop: SQL files are streamed directly from disk, no server upload cap applies.
|
||||
}
|
||||
|
||||
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
|
||||
return invoke("save_max_agent_turns", { maxAgentTurns });
|
||||
}
|
||||
@@ -970,16 +978,16 @@ export type ExternalSqlFileStatus = { kind: "present"; sizeBytes: number; modifi
|
||||
|
||||
export type ExternalSqlFileWriteResult = { kind: "written"; version: ExternalSqlFileVersion } | { kind: "conflict"; currentVersion: ExternalSqlFileVersion } | { kind: "missing" };
|
||||
|
||||
export async function readExternalSqlFileSnapshot(path: string): Promise<ExternalSqlFileSnapshot> {
|
||||
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path });
|
||||
export async function readExternalSqlFileSnapshot(path: string, maxSizeBytes?: number): Promise<ExternalSqlFileSnapshot> {
|
||||
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path, maxSizeBytes });
|
||||
if (result.kind === "tooLarge") {
|
||||
throw new ExternalSqlFileTooLargeError(result.sizeBytes, result.maxSizeBytes);
|
||||
}
|
||||
return { content: result.content, version: result.version };
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(path: string): Promise<string> {
|
||||
return (await readExternalSqlFileSnapshot(path)).content;
|
||||
export async function readExternalSqlFile(path: string, maxSizeBytes?: number): Promise<string> {
|
||||
return (await readExternalSqlFileSnapshot(path, maxSizeBytes)).content;
|
||||
}
|
||||
|
||||
export async function inspectExternalSqlFile(path: string): Promise<ExternalSqlFileStatus> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { EditorSettings } from "@/stores/settingsStore";
|
||||
import { normalizeBackgroundImageSettings } from "@/lib/app/appBackgroundImage";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen";
|
||||
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
|
||||
@@ -59,6 +60,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"tableOpenPageSize",
|
||||
"queryResultMaxRowsEnabled",
|
||||
"queryResultMaxRows",
|
||||
"externalSqlEditorMaxMb",
|
||||
"infiniteScroll",
|
||||
"regexMaxMatchCount",
|
||||
"autoCalculateTotalRows",
|
||||
@@ -131,6 +133,7 @@ export function normalizeQueryResultMaxRowsDraft(value: unknown): number {
|
||||
function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unknown {
|
||||
if (key === "pageSize" || key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value);
|
||||
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
|
||||
if (key === "externalSqlEditorMaxMb") return normalizeExternalSqlEditorMaxMb(value);
|
||||
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
|
||||
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
|
||||
if (key === "backgroundImage") return normalizeBackgroundImageSettings(value);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "sqlFile" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
|
||||
export interface SettingsSearchContext {
|
||||
isWeb: boolean;
|
||||
@@ -199,6 +199,8 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
|
||||
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data-grid-filter-view" },
|
||||
{ id: "data-grid-flattening-multi-line", category: "data", titleKey: "settings.flatteningMultiLineText", descriptionKey: "settings.flatteningMultiLineTextDescription", targetId: "data" },
|
||||
{ id: "sql-file-editor-max-mb", category: "sqlFile", titleKey: "settings.externalSqlEditorMaxMb", descriptionKey: "settings.externalSqlEditorMaxMbDescription", targetId: "sqlFile" },
|
||||
{ id: "sql-file-web-upload-max-mb", category: "sqlFile", titleKey: "settings.webSqlFileUploadMaxMb", descriptionKey: "settings.webSqlFileUploadMaxMbDescription", targetId: "sqlFile", visible: (context) => context.isWeb },
|
||||
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
|
||||
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },
|
||||
...createToolbarVisibilitySettingsSearchDefinitions(),
|
||||
|
||||
@@ -152,6 +152,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
|
||||
"tableOpenPageSize",
|
||||
"queryResultMaxRowsEnabled",
|
||||
"queryResultMaxRows",
|
||||
"externalSqlEditorMaxMb",
|
||||
"infiniteScroll",
|
||||
"regexMaxMatchCount",
|
||||
"autoCalculateTotalRows",
|
||||
|
||||
@@ -49,6 +49,25 @@ export function externalSqlFilePaths(paths: string[]): string[] {
|
||||
|
||||
export const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
export const MIN_EXTERNAL_SQL_EDITOR_FILE_MB = 1;
|
||||
export const MAX_EXTERNAL_SQL_EDITOR_FILE_MB = 4096;
|
||||
|
||||
export function normalizeExternalSqlEditorMaxMb(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES / (1024 * 1024);
|
||||
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, Math.round(parsed)));
|
||||
}
|
||||
|
||||
export function externalSqlEditorMaxBytes(maxMb: number): number {
|
||||
return Math.round(maxMb * 1024 * 1024);
|
||||
}
|
||||
|
||||
export function clampExternalSqlEditorMaxMbInput(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return MIN_EXTERNAL_SQL_EDITOR_FILE_MB;
|
||||
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, Math.round(parsed)));
|
||||
}
|
||||
|
||||
export class ExternalSqlFileTooLargeError extends Error {
|
||||
constructor(
|
||||
readonly sizeBytes: number,
|
||||
@@ -63,9 +82,9 @@ export function isExternalSqlFileTooLargeError(error: unknown): error is Externa
|
||||
return error instanceof ExternalSqlFileTooLargeError;
|
||||
}
|
||||
|
||||
export function readBrowserSqlFile(file: Blob): Promise<string> {
|
||||
if (file.size > MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES) {
|
||||
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES));
|
||||
export function readBrowserSqlFile(file: Blob, maxSizeBytes = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES): Promise<string> {
|
||||
if (file.size > maxSizeBytes) {
|
||||
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, maxSizeBytes));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -436,6 +436,14 @@ describe("normalizeEditorSettings", () => {
|
||||
expect(normalizeEditorSettings({ timeoutInheritanceMigrationVersion: 2 }).timeoutInheritanceMigrationVersion).toBe(2);
|
||||
});
|
||||
|
||||
it("normalizes the external SQL editor size limit", () => {
|
||||
expect(normalizeEditorSettings({}).externalSqlEditorMaxMb).toBe(64);
|
||||
expect(normalizeEditorSettings({ externalSqlEditorMaxMb: 0 }).externalSqlEditorMaxMb).toBe(1);
|
||||
expect(normalizeEditorSettings({ externalSqlEditorMaxMb: 256 }).externalSqlEditorMaxMb).toBe(256);
|
||||
expect(normalizeEditorSettings({ externalSqlEditorMaxMb: 99999 }).externalSqlEditorMaxMb).toBe(4096);
|
||||
expect(normalizeEditorSettings({ externalSqlEditorMaxMb: "128" } as any).externalSqlEditorMaxMb).toBe(128);
|
||||
});
|
||||
|
||||
it("normalizes toolbar item settings from older saved settings", () => {
|
||||
const settings = normalizeEditorSettings({
|
||||
toolbarItems: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT, normalizeDataGridTextFilter
|
||||
import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorScheme, normalizeActiveDataGridTypeColorSchemeId, normalizeDataGridTypeColorSchemes } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen";
|
||||
import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits";
|
||||
import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
|
||||
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
@@ -768,6 +769,7 @@ export interface EditorSettings {
|
||||
tableOpenPageSize: number;
|
||||
queryResultMaxRowsEnabled: boolean;
|
||||
queryResultMaxRows: number;
|
||||
externalSqlEditorMaxMb: number;
|
||||
infiniteScroll: boolean;
|
||||
/** Preserved for downgrade compatibility; current clients use queryResultMaxRows. */
|
||||
infiniteScrollMaxRows: number;
|
||||
@@ -1004,6 +1006,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
tableOpenPageSize: 100,
|
||||
queryResultMaxRowsEnabled: true,
|
||||
queryResultMaxRows: DEFAULT_QUERY_RESULT_MAX_ROWS,
|
||||
externalSqlEditorMaxMb: 64,
|
||||
infiniteScroll: false,
|
||||
infiniteScrollMaxRows: 5000,
|
||||
flatteningMultiLineText: false,
|
||||
@@ -1458,6 +1461,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
tableOpenPageSize: normalizeResultPageSize(settings.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize),
|
||||
queryResultMaxRowsEnabled: settings.queryResultMaxRowsEnabled !== false,
|
||||
queryResultMaxRows: normalizeQueryResultMaxRows(settings.queryResultMaxRows),
|
||||
externalSqlEditorMaxMb: normalizeExternalSqlEditorMaxMb(settings.externalSqlEditorMaxMb),
|
||||
infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll,
|
||||
infiniteScrollMaxRows: typeof settings.infiniteScrollMaxRows === "number" && settings.infiniteScrollMaxRows >= 1000 && settings.infiniteScrollMaxRows <= 50000 ? Math.round(settings.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows,
|
||||
flatteningMultiLineText: settings.flatteningMultiLineText ?? DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText,
|
||||
@@ -2184,6 +2188,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.tableOpenPageSize !== undefined) editorSettings.value.tableOpenPageSize = normalizeResultPageSize(partial.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize);
|
||||
if (partial.queryResultMaxRowsEnabled !== undefined) editorSettings.value.queryResultMaxRowsEnabled = Boolean(partial.queryResultMaxRowsEnabled);
|
||||
if (partial.queryResultMaxRows !== undefined) editorSettings.value.queryResultMaxRows = normalizeQueryResultMaxRows(partial.queryResultMaxRows, editorSettings.value.queryResultMaxRows);
|
||||
if (partial.externalSqlEditorMaxMb !== undefined) editorSettings.value.externalSqlEditorMaxMb = normalizeExternalSqlEditorMaxMb(partial.externalSqlEditorMaxMb);
|
||||
if (partial.infiniteScroll !== undefined) editorSettings.value.infiniteScroll = partial.infiniteScroll;
|
||||
if (partial.infiniteScrollMaxRows !== undefined)
|
||||
editorSettings.value.infiniteScrollMaxRows = typeof partial.infiniteScrollMaxRows === "number" && partial.infiniteScrollMaxRows >= 1000 && partial.infiniteScrollMaxRows <= 50000 ? Math.round(partial.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
|
||||
|
||||
@@ -9,6 +9,13 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::connection::{AppState, PoolKind};
|
||||
use crate::db;
|
||||
use crate::models::connection::DatabaseType;
|
||||
|
||||
pub const DEFAULT_SQL_FILE_UPLOAD_MAX_MB: u32 = 200;
|
||||
pub const MAX_SQL_FILE_UPLOAD_MAX_MB: u32 = 4096;
|
||||
|
||||
pub fn clamp_sql_file_upload_max_mb(value: u32) -> u32 {
|
||||
value.clamp(1, MAX_SQL_FILE_UPLOAD_MAX_MB)
|
||||
}
|
||||
use crate::query::{
|
||||
execute_sql_statement_with_options, pool_error_action, wait_for_query_opt, DbOperationBudget, PoolErrorAction,
|
||||
QueryExecutionOptions,
|
||||
|
||||
@@ -38,6 +38,7 @@ const APP_STATE_TRANSFER_TASK_LIBRARY_KEY: &str = "transfer_task_library";
|
||||
const MCP_GLOBAL_POLICY_KEY: &str = "mcp_global_policy";
|
||||
const MCP_HTTP_SERVER_SETTINGS_KEY: &str = "mcp_http_server_settings";
|
||||
const MAX_RETRIES_KEY: &str = "max_retries";
|
||||
const SQL_FILE_UPLOAD_MAX_MB_KEY: &str = "sql_file_upload_max_mb";
|
||||
const APP_STATE_AI_GLOBAL_INSTRUCTIONS_KEY: &str = "ai_global_custom_instructions";
|
||||
const APP_STATE_AI_CHAT_SELECTION_KEY: &str = "ai_chat_selection_v1";
|
||||
const SNIPPET_SYNC_IDS_KEY: &str = "snippet_sync_ids";
|
||||
@@ -2050,7 +2051,7 @@ impl Storage {
|
||||
.query_row("SELECT settings_json FROM app_settings WHERE id = 1", [], |row| row.get(0))
|
||||
.optional()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let dedicated_keys = [MCP_GLOBAL_POLICY_KEY, MAX_RETRIES_KEY];
|
||||
let dedicated_keys = [MCP_GLOBAL_POLICY_KEY, MAX_RETRIES_KEY, SQL_FILE_UPLOAD_MAX_MB_KEY];
|
||||
for key in dedicated_keys {
|
||||
settings.remove(key);
|
||||
}
|
||||
@@ -2663,6 +2664,39 @@ impl Storage {
|
||||
.map(|value| crate::ai::clamp_max_retries(value.min(u32::MAX as u64) as u32))
|
||||
.unwrap_or(crate::ai::DEFAULT_MAX_RETRIES))
|
||||
}
|
||||
|
||||
pub async fn save_sql_file_upload_max_mb(&self, max_mb: u32) -> Result<(), String> {
|
||||
let max_mb = crate::sql_file_import::clamp_sql_file_upload_max_mb(max_mb);
|
||||
self.with_conn(move |conn| {
|
||||
let current: Option<String> = conn
|
||||
.query_row("SELECT settings_json FROM app_settings WHERE id = 1", [], |row| row.get(0))
|
||||
.optional()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut settings = match current {
|
||||
Some(json) => serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&json)
|
||||
.map_err(|e| format!("invalid app settings JSON: {e}"))?,
|
||||
None => serde_json::Map::new(),
|
||||
};
|
||||
settings.insert(
|
||||
SQL_FILE_UPLOAD_MAX_MB_KEY.to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(max_mb)),
|
||||
);
|
||||
let json = serde_json::Value::Object(settings).to_string();
|
||||
conn.execute("INSERT OR REPLACE INTO app_settings (id, settings_json) VALUES (1, ?1)", [json])
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn load_sql_file_upload_max_mb(&self) -> Result<u32, String> {
|
||||
let settings = self.load_app_settings_json().await?;
|
||||
Ok(settings
|
||||
.get(SQL_FILE_UPLOAD_MAX_MB_KEY)
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|value| crate::sql_file_import::clamp_sql_file_upload_max_mb(value.min(u32::MAX as u64) as u32))
|
||||
.unwrap_or(crate::sql_file_import::DEFAULT_SQL_FILE_UPLOAD_MAX_MB))
|
||||
}
|
||||
}
|
||||
|
||||
// AI Conversations
|
||||
@@ -7197,6 +7231,39 @@ mod tests {
|
||||
assert_eq!(storage.load_max_retries().await.unwrap(), 7);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_file_upload_max_mb_defaults_and_persists_clamped() {
|
||||
let path = temp_db_path("sql-file-upload-max-mb");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
storage.load_sql_file_upload_max_mb().await.unwrap(),
|
||||
crate::sql_file_import::DEFAULT_SQL_FILE_UPLOAD_MAX_MB
|
||||
);
|
||||
|
||||
storage.save_sql_file_upload_max_mb(512).await.unwrap();
|
||||
assert_eq!(storage.load_sql_file_upload_max_mb().await.unwrap(), 512);
|
||||
|
||||
// Values above the cap are clamped so raw DB edits cannot bypass the limit.
|
||||
storage.save_sql_file_upload_max_mb(u32::MAX).await.unwrap();
|
||||
assert_eq!(
|
||||
storage.load_sql_file_upload_max_mb().await.unwrap(),
|
||||
crate::sql_file_import::MAX_SQL_FILE_UPLOAD_MAX_MB
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_file_upload_max_mb_survives_stale_app_settings_save() {
|
||||
let path = temp_db_path("sql-file-upload-max-mb-stale-save");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
let stale_settings = storage.load_app_settings_json().await.unwrap();
|
||||
|
||||
storage.save_sql_file_upload_max_mb(256).await.unwrap();
|
||||
storage.save_app_settings_json(&stale_settings).await.unwrap();
|
||||
|
||||
assert_eq!(storage.load_sql_file_upload_max_mb().await.unwrap(), 256);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_hash_preserves_existing_desktop_settings() {
|
||||
let path = temp_db_path("password-preserve-desktop-settings");
|
||||
|
||||
@@ -1016,7 +1016,9 @@ async fn main() {
|
||||
.route(
|
||||
"/sql-file/preview",
|
||||
post(routes::sql_file::preview_sql_file)
|
||||
.layer(DefaultBodyLimit::max(routes::sql_file::SQL_FILE_UPLOAD_MAX_BYTES.saturating_add(1024 * 1024))),
|
||||
// Upper bound only; the effective (possibly lower) limit configured via
|
||||
// Settings > SQL File Size is enforced inside the handler at request time.
|
||||
.layer(DefaultBodyLimit::max(routes::sql_file::sql_file_upload_hard_cap_bytes())),
|
||||
)
|
||||
.route("/sql-file/execute", post(routes::sql_file::execute_sql_file))
|
||||
.route("/sql-file/progress/{executionId}", get(routes::sql_file::sql_file_progress))
|
||||
@@ -1057,6 +1059,11 @@ async fn main() {
|
||||
"/app-settings/max-retries",
|
||||
get(routes::app_settings::load_max_retries).put(routes::app_settings::save_max_retries),
|
||||
)
|
||||
.route(
|
||||
"/app-settings/sql-file-upload-max-bytes",
|
||||
get(routes::app_settings::load_sql_file_upload_max_bytes)
|
||||
.put(routes::app_settings::save_sql_file_upload_max_mb),
|
||||
)
|
||||
.route("/app-settings/config/decrypt", post(routes::app_settings::decrypt_config))
|
||||
// Cloud sync
|
||||
.route("/cloud-sync/webdav/test", post(routes::cloud_sync::webdav_sync_test))
|
||||
|
||||
@@ -130,6 +130,29 @@ pub async fn load_max_retries(State(state): State<Arc<WebState>>) -> Result<Json
|
||||
state.app.storage.load_max_retries().await.map(Json).map_err(AppError::from)
|
||||
}
|
||||
|
||||
fn sql_file_upload_max_bytes_from_mb(max_mb: u32) -> u64 {
|
||||
u64::from(max_mb).saturating_mul(1024 * 1024)
|
||||
}
|
||||
|
||||
pub async fn load_sql_file_upload_max_bytes(State(state): State<Arc<WebState>>) -> Result<Json<u64>, AppError> {
|
||||
let max_mb = state.app.storage.load_sql_file_upload_max_mb().await.map_err(AppError::from)?;
|
||||
Ok(Json(sql_file_upload_max_bytes_from_mb(max_mb)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveSqlFileUploadMaxMbRequest {
|
||||
pub sql_file_upload_max_mb: u32,
|
||||
}
|
||||
|
||||
pub async fn save_sql_file_upload_max_mb(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<SaveSqlFileUploadMaxMbRequest>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
state.app.storage.save_sql_file_upload_max_mb(body.sql_file_upload_max_mb).await.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn save_max_retries(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<SaveMaxRetriesRequest>,
|
||||
@@ -163,7 +186,12 @@ fn decrypt_config_payload(payload: &EncryptedConfigPayload, passphrase: &str) ->
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{decrypt_config_payload, EncryptedConfigPayload};
|
||||
use super::{decrypt_config_payload, sql_file_upload_max_bytes_from_mb, EncryptedConfigPayload};
|
||||
|
||||
#[test]
|
||||
fn preserves_four_gib_sql_file_upload_limit() {
|
||||
assert_eq!(sql_file_upload_max_bytes_from_mb(4096), 4096_u64 * 1024 * 1024);
|
||||
}
|
||||
|
||||
fn exported_browser_payload() -> EncryptedConfigPayload {
|
||||
EncryptedConfigPayload {
|
||||
|
||||
@@ -23,6 +23,31 @@ use crate::state::WebState;
|
||||
const PENDING_SQL_FILE_PROGRESS_CHANNEL_TTL: Duration = Duration::from_secs(30);
|
||||
const SQL_FILE_UPLOAD_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
pub const SQL_FILE_UPLOAD_MAX_BYTES: usize = 200 * 1024 * 1024;
|
||||
const SQL_FILE_UPLOAD_MIN_BYTES: usize = 1024 * 1024;
|
||||
const SQL_FILE_UPLOAD_MAX_BYTES_LIMIT: usize = 4096 * 1024 * 1024;
|
||||
|
||||
pub fn sql_file_upload_max_bytes_from_value(value: Option<&str>) -> usize {
|
||||
let mb = value
|
||||
.and_then(|value| value.trim().parse::<usize>().ok())
|
||||
.filter(|mb| *mb > 0)
|
||||
.map(|mb| mb.saturating_mul(1024 * 1024))
|
||||
.unwrap_or(SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
mb.clamp(SQL_FILE_UPLOAD_MIN_BYTES, SQL_FILE_UPLOAD_MAX_BYTES_LIMIT)
|
||||
}
|
||||
|
||||
pub fn sql_file_upload_hard_cap_bytes() -> usize {
|
||||
SQL_FILE_UPLOAD_MAX_BYTES_LIMIT.saturating_add(1024 * 1024)
|
||||
}
|
||||
|
||||
async fn sql_file_upload_limit(state: &WebState) -> usize {
|
||||
let max_mb = state
|
||||
.app
|
||||
.storage
|
||||
.load_sql_file_upload_max_mb()
|
||||
.await
|
||||
.unwrap_or(dbx_core::sql_file_import::DEFAULT_SQL_FILE_UPLOAD_MAX_MB);
|
||||
sql_file_upload_max_bytes_from_value(Some(&max_mb.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -49,12 +74,9 @@ pub async fn preview_sql_file(
|
||||
if let Some(field) = multipart.next_field().await.map_err(|e| AppError::from(e.to_string()))? {
|
||||
let file_name = field.file_name().unwrap_or("upload.sql").to_string();
|
||||
let data = field.bytes().await.map_err(|e| AppError::from(e.to_string()))?;
|
||||
if data.len() > SQL_FILE_UPLOAD_MAX_BYTES {
|
||||
return Err(AppError::from(format!(
|
||||
"File too large: {} bytes (max {} bytes)",
|
||||
data.len(),
|
||||
SQL_FILE_UPLOAD_MAX_BYTES
|
||||
)));
|
||||
let upload_limit = sql_file_upload_limit(&state).await;
|
||||
if data.len() > upload_limit {
|
||||
return Err(AppError::from(format!("File too large: {} bytes (max {} bytes)", data.len(), upload_limit)));
|
||||
}
|
||||
|
||||
let file_path = safe_uploaded_sql_path(&tmp_dir, &file_name)?;
|
||||
@@ -135,12 +157,13 @@ pub async fn execute_sql_file(
|
||||
None,
|
||||
));
|
||||
for file_path in &file_paths {
|
||||
let upload_limit = sql_file_upload_limit(&state_clone).await;
|
||||
match std::fs::metadata(file_path) {
|
||||
Ok(meta) if meta.len() > SQL_FILE_UPLOAD_MAX_BYTES as u64 => {
|
||||
Ok(meta) if meta.len() > upload_limit as u64 => {
|
||||
progress_emitter.emit(sql_file_error_progress(
|
||||
&req.execution_id,
|
||||
started_at,
|
||||
format!("File too large: {} bytes (max {} bytes)", meta.len(), SQL_FILE_UPLOAD_MAX_BYTES),
|
||||
format!("File too large: {} bytes (max {} bytes)", meta.len(), upload_limit),
|
||||
));
|
||||
cleanup_sql_file_execution(&state_clone, &req.execution_id).await;
|
||||
return;
|
||||
@@ -278,7 +301,20 @@ pub async fn cancel_sql_file(
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{cleanup_sql_file_uploads_older_than, safe_uploaded_sql_path, validated_uploaded_sql_path};
|
||||
use super::{
|
||||
cleanup_sql_file_uploads_older_than, safe_uploaded_sql_path, sql_file_upload_max_bytes_from_value,
|
||||
validated_uploaded_sql_path, SQL_FILE_UPLOAD_MAX_BYTES,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn sql_file_upload_limit_defaults_and_clamps() {
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(None), SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("")), SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("0")), SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("512")), 512 * 1024 * 1024);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("1")), 1024 * 1024);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("99999")), 4096 * 1024 * 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uploaded_sql_paths_are_unique_and_keep_the_extension() {
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ test("cold-start SQL files wait for restored tabs before opening", () => {
|
||||
|
||||
const openPathSource = appSource.slice(openPathStart, openPathEnd);
|
||||
const initializationWait = openPathSource.indexOf("await desktopOpenTabsRestorationBarrier?.settled");
|
||||
const fileRead = openPathSource.indexOf("api.readExternalSqlFileSnapshot(path)");
|
||||
const fileRead = openPathSource.indexOf("api.readExternalSqlFileSnapshot(path, externalSqlEditorMaxBytes(settingsStore.editorSettings.externalSqlEditorMaxMb))");
|
||||
const tabOpen = openPathSource.indexOf("queryStore.openExternalSqlFile");
|
||||
assert.ok(initializationWait >= 0);
|
||||
assert.ok(initializationWait < fileRead);
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ test("SQL library imports use the charset-aware external SQL reader", () => {
|
||||
const end = source.indexOf("async function chooseSyncDirectory", start);
|
||||
const handler = start >= 0 && end > start ? source.slice(start, end) : "";
|
||||
|
||||
assert.match(handler, /await api\.readExternalSqlFile\(path\)/);
|
||||
assert.match(handler, /await api\.readExternalSqlFile\(path, externalSqlEditorMaxBytes\(settingsStore\.editorSettings\.externalSqlEditorMaxMb\)\)/);
|
||||
assert.doesNotMatch(handler, /readTextFile/);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,9 +8,18 @@ use sha2::{Digest, Sha256};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES: u64 = 64 * 1024 * 1024;
|
||||
const MIN_EXTERNAL_SQL_EDITOR_FILE_BYTES: u64 = 1024 * 1024;
|
||||
const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES_LIMIT: u64 = 4096 * 1024 * 1024;
|
||||
|
||||
fn exceeds_external_sql_editor_limit(size_bytes: u64) -> bool {
|
||||
size_bytes > MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES
|
||||
fn clamp_external_sql_editor_limit(max_size_bytes: Option<u64>) -> u64 {
|
||||
match max_size_bytes {
|
||||
Some(value) => value.clamp(MIN_EXTERNAL_SQL_EDITOR_FILE_BYTES, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES_LIMIT),
|
||||
None => MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
}
|
||||
}
|
||||
|
||||
fn exceeds_external_sql_editor_limit(size_bytes: u64, max_size_bytes: u64) -> bool {
|
||||
size_bytes > max_size_bytes
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
@@ -60,8 +69,11 @@ pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) ->
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_external_sql_file(path: String) -> Result<ExternalSqlFileReadResult, String> {
|
||||
read_external_sql_file_content_async(PathBuf::from(path)).await
|
||||
pub async fn read_external_sql_file(
|
||||
path: String,
|
||||
max_size_bytes: Option<u64>,
|
||||
) -> Result<ExternalSqlFileReadResult, String> {
|
||||
read_external_sql_file_content_async(PathBuf::from(path), clamp_external_sql_editor_limit(max_size_bytes)).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -181,43 +193,42 @@ fn external_sql_file_version(metadata: &std::fs::Metadata, bytes: &[u8]) -> Exte
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_external_sql_file_content(path: &Path) -> Result<ExternalSqlFileReadResult, String> {
|
||||
read_external_sql_file_content_with_limit(path, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_external_sql_file_content_with_limit(
|
||||
path: &Path,
|
||||
max_size_bytes: u64,
|
||||
) -> Result<ExternalSqlFileReadResult, String> {
|
||||
let metadata = std::fs::metadata(path).map_err(|e| format!("Failed to inspect SQL file: {e}"))?;
|
||||
if !metadata.is_file() {
|
||||
return Err("Only files can be opened this way".to_string());
|
||||
}
|
||||
if exceeds_external_sql_editor_limit(metadata.len()) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: metadata.len(),
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
if exceeds_external_sql_editor_limit(metadata.len(), max_size_bytes) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge { size_bytes: metadata.len(), max_size_bytes });
|
||||
}
|
||||
let bytes = std::fs::read(path).map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
let version = external_sql_file_version(&metadata, &bytes);
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content { content, version })
|
||||
}
|
||||
|
||||
async fn read_external_sql_file_content_async(path: PathBuf) -> Result<ExternalSqlFileReadResult, String> {
|
||||
async fn read_external_sql_file_content_async(
|
||||
path: PathBuf,
|
||||
max_size_bytes: u64,
|
||||
) -> Result<ExternalSqlFileReadResult, String> {
|
||||
let file = tokio::fs::File::open(&path).await.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
let metadata = file.metadata().await.map_err(|e| format!("Failed to inspect SQL file: {e}"))?;
|
||||
if !metadata.is_file() {
|
||||
return Err("Only files can be opened this way".to_string());
|
||||
}
|
||||
if exceeds_external_sql_editor_limit(metadata.len()) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: metadata.len(),
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
if exceeds_external_sql_editor_limit(metadata.len(), max_size_bytes) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge { size_bytes: metadata.len(), max_size_bytes });
|
||||
}
|
||||
let mut bytes = Vec::with_capacity(metadata.len() as usize);
|
||||
file.take(MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
if exceeds_external_sql_editor_limit(bytes.len() as u64) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: bytes.len() as u64,
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
file.take(max_size_bytes + 1).read_to_end(&mut bytes).await.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
if exceeds_external_sql_editor_limit(bytes.len() as u64, max_size_bytes) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge { size_bytes: bytes.len() as u64, max_size_bytes });
|
||||
}
|
||||
let version = external_sql_file_version(&metadata, &bytes);
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content { content, version })
|
||||
@@ -413,8 +424,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn external_sql_editor_limit_is_inclusive() {
|
||||
assert!(!exceeds_external_sql_editor_limit(MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES));
|
||||
assert!(exceeds_external_sql_editor_limit(MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1));
|
||||
assert!(!exceeds_external_sql_editor_limit(
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES
|
||||
));
|
||||
assert!(exceeds_external_sql_editor_limit(
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1,
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamps_external_sql_editor_limit_from_frontend() {
|
||||
assert_eq!(clamp_external_sql_editor_limit(None), MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES);
|
||||
assert_eq!(clamp_external_sql_editor_limit(Some(0)), MIN_EXTERNAL_SQL_EDITOR_FILE_BYTES);
|
||||
assert_eq!(clamp_external_sql_editor_limit(Some(16 * 1024 * 1024)), 16 * 1024 * 1024);
|
||||
assert_eq!(clamp_external_sql_editor_limit(Some(u64::MAX)), MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES_LIMIT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -436,6 +461,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversized_external_sql_with_custom_limit() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
std::fs::write(&path, "select 1;").unwrap();
|
||||
|
||||
let result = read_external_sql_file_content_with_limit(&path, 4);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(result.unwrap(), ExternalSqlFileReadResult::TooLarge { size_bytes: 9, max_size_bytes: 4 });
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_oversized_external_sql_in_async_command_path() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
@@ -443,7 +479,7 @@ mod tests {
|
||||
let file_size = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1;
|
||||
file.set_len(file_size).unwrap();
|
||||
|
||||
let result = read_external_sql_file_content_async(path.clone()).await;
|
||||
let result = read_external_sql_file_content_async(path.clone(), MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES).await;
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user