diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue
index 767a5f560b..b04c7e66b6 100644
--- a/apps/desktop/src/components/connection/ConnectionDialog.vue
+++ b/apps/desktop/src/components/connection/ConnectionDialog.vue
@@ -33,6 +33,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
+import { MAX_CONNECT_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection";
import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseServiceConnectionUrl, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connection/connectionPresentation";
@@ -3703,10 +3704,9 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
// service, SID, and descriptor JDBC strings exactly as before.
config.connection_string = undefined;
}
- const connectTimeout = Number(config.connect_timeout_secs);
- config.connect_timeout_secs = config.connect_timeout_inherit === true ? normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value) : Number.isFinite(connectTimeout) && connectTimeout > 0 ? connectTimeout : 10;
+ config.connect_timeout_secs = config.connect_timeout_inherit === true ? normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value) : normalizeGlobalConnectTimeoutSecs(config.connect_timeout_secs);
const queryTimeout = Number(config.query_timeout_secs);
- config.query_timeout_secs = config.query_timeout_inherit === true ? normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value) : Number.isFinite(queryTimeout) && queryTimeout >= 0 ? queryTimeout : 30;
+ config.query_timeout_secs = config.query_timeout_inherit === true ? normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value) : normalizeGlobalQueryTimeoutSecs(queryTimeout);
const idleTimeout = Number(config.idle_timeout_secs);
config.idle_timeout_secs = Number.isFinite(idleTimeout) && idleTimeout >= 0 ? idleTimeout : 60;
const keepaliveInterval = Number(config.keepalive_interval_secs);
@@ -5007,6 +5007,26 @@ function validateTransportLayers(config: LegacyConnectionConfig) {
});
}
+function clampQueryTimeoutInput(event: Event, target: "global" | "connection") {
+ const input = event.target as HTMLInputElement;
+ if (input.value === "") return;
+ const value = Number(input.value);
+ if (!Number.isFinite(value) || value <= MAX_QUERY_TIMEOUT_SECS) return;
+ input.value = String(MAX_QUERY_TIMEOUT_SECS);
+ if (target === "global") editGlobalQueryTimeoutSecs.value = MAX_QUERY_TIMEOUT_SECS;
+ else form.value.query_timeout_secs = MAX_QUERY_TIMEOUT_SECS;
+}
+
+function clampConnectTimeoutInput(event: Event, target: "global" | "connection") {
+ const input = event.target as HTMLInputElement;
+ if (input.value === "") return;
+ const value = Number(input.value);
+ if (!Number.isFinite(value) || value <= MAX_CONNECT_TIMEOUT_SECS) return;
+ input.value = String(MAX_CONNECT_TIMEOUT_SECS);
+ if (target === "global") editGlobalConnectTimeoutSecs.value = MAX_CONNECT_TIMEOUT_SECS;
+ else form.value.connect_timeout_secs = MAX_CONNECT_TIMEOUT_SECS;
+}
+
async function persistGlobalTimeoutDrafts() {
const nextConnect = normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value);
const nextQuery = normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value);
@@ -7728,13 +7748,41 @@ function openExternalUrl(url: string) {
@@ -7750,13 +7798,23 @@ function openExternalUrl(url: string) {
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 85b234e681..3211a5240e 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1012,6 +1012,8 @@ export default {
connectTimeout: "Connection Timeout (seconds)",
queryTimeout: "Query Timeout (seconds)",
useGlobalQueryTimeout: "Global",
+ globalConnectTimeoutHint: "Range: 1 to 300 seconds",
+ globalQueryTimeoutHint: "Range: 0 to 3600 seconds. Set to 0 for no timeout.",
useConnectionQueryTimeout: "Connection",
idleTimeout: "Idle Timeout (seconds)",
keepaliveInterval: "Keepalive Interval (seconds)",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index a3f8fa6201..fcc5bc137d 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -795,6 +795,8 @@ export default withEnglishFallback({
connectTimeout: "Tiempo de espera de conexión (segundos)",
queryTimeout: "Tiempo de espera de consulta (segundos)",
useGlobalQueryTimeout: "Global",
+ globalConnectTimeoutHint: "Rango: de 1 a 300 segundos",
+ globalQueryTimeoutHint: "Rango: de 0 a 3600 segundos. Establece 0 para no aplicar límite.",
useConnectionQueryTimeout: "Conexión",
idleTimeout: "Tiempo de espera inactivo (segundos)",
keepaliveInterval: "Intervalo de keepalive (segundos)",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 6fb5d0552a..9393c9ebae 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -793,6 +793,8 @@ export default withEnglishFallback({
connectTimeout: "Timeout Connessione (secondi)",
queryTimeout: "Timeout Query (secondi)",
useGlobalQueryTimeout: "Globale",
+ globalConnectTimeoutHint: "Intervallo: da 1 a 300 secondi",
+ globalQueryTimeoutHint: "Intervallo: da 0 a 3600 secondi. Imposta 0 per nessun limite.",
useConnectionQueryTimeout: "Connessione",
idleTimeout: "Timeout Inattività (secondi)",
keepaliveInterval: "Intervallo keepalive (secondi)",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 821ba39ce6..838291ea03 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -759,6 +759,8 @@ export default withEnglishFallback({
connectTimeout: "接続タイムアウト(秒)",
queryTimeout: "クエリタイムアウト(秒)",
useGlobalQueryTimeout: "グローバル",
+ globalConnectTimeoutHint: "1~300 秒の範囲で指定します",
+ globalQueryTimeoutHint: "0~3600 秒の範囲で指定します。0 はタイムアウトなしです。",
useConnectionQueryTimeout: "接続",
idleTimeout: "アイドルタイムアウト(秒)",
keepaliveInterval: "Keepalive 間隔(秒)",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 759a2c027e..ccae270362 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -909,6 +909,8 @@ export default withEnglishFallback({
connectTimeout: "연결 제한 시간 (초)",
queryTimeout: "쿼리 제한 시간 (초)",
useGlobalQueryTimeout: "전역",
+ globalConnectTimeoutHint: "범위: 1~300초",
+ globalQueryTimeoutHint: "범위: 0~3600초. 0으로 설정하면 제한이 없습니다.",
useConnectionQueryTimeout: "연결",
idleTimeout: "유휴 제한 시간 (초)",
keepaliveInterval: "킵얼라이브 간격 (초)",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 3dcf5692c1..2e029d6be8 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -794,6 +794,8 @@ export default withEnglishFallback({
connectTimeout: "Timeout de Conexão (segundos)",
queryTimeout: "Timeout de Consulta (segundos)",
useGlobalQueryTimeout: "Global",
+ globalConnectTimeoutHint: "Intervalo: de 1 a 300 segundos",
+ globalQueryTimeoutHint: "Intervalo: de 0 a 3600 segundos. Defina 0 para não haver limite.",
useConnectionQueryTimeout: "Conexão",
idleTimeout: "Timeout de Inatividade (segundos)",
keepaliveInterval: "Intervalo de keepalive (segundos)",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index ec0746bba7..63c0a16b73 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -940,6 +940,8 @@ export default withEnglishFallback({
connectTimeout: "连接超时(秒)",
queryTimeout: "查询超时(秒)",
useGlobalQueryTimeout: "全局",
+ globalConnectTimeoutHint: "范围为1~300秒",
+ globalQueryTimeoutHint: "范围为0~3600秒。设为0表示不限制",
useConnectionQueryTimeout: "当前连接",
idleTimeout: "空闲超时(秒)",
keepaliveInterval: "保持连接间隔(秒)",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index d48d29e89a..291289f999 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -793,6 +793,8 @@ export default withEnglishFallback({
connectTimeout: "連線逾時(秒)",
queryTimeout: "查詢逾時(秒)",
useGlobalQueryTimeout: "全域",
+ globalConnectTimeoutHint: "範圍為1~300秒",
+ globalQueryTimeoutHint: "範圍為0~3600秒。設為0表示不限制",
useConnectionQueryTimeout: "目前連線",
idleTimeout: "閒置逾時(秒)",
keepaliveInterval: "保持連線間隔(秒)",
diff --git a/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts
new file mode 100644
index 0000000000..0a6bd458c7
--- /dev/null
+++ b/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts
@@ -0,0 +1,32 @@
+import { readFileSync } from "node:fs";
+import { describe, expect, it } from "vitest";
+import { DEFAULT_CONNECT_TIMEOUT_SECS, normalizeConnectTimeoutSecs } from "@/lib/connection/timeoutLimits";
+
+const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
+
+describe("ConnectionDialog timeout controls", () => {
+ it("uses separate maximums for connection and query timeouts", () => {
+ expect(dialogSource).toMatch(/v-model\.number="editGlobalConnectTimeoutSecs"[\s\S]*?min="1"[\s\S]*?:max="MAX_CONNECT_TIMEOUT_SECS"[\s\S]*?step="1"/);
+ expect(dialogSource).toMatch(/v-model\.number="form\.connect_timeout_secs"[\s\S]*?min="1"[\s\S]*?:max="MAX_CONNECT_TIMEOUT_SECS"[\s\S]*?step="1"/);
+ expect(dialogSource).toMatch(/v-model\.number="editGlobalQueryTimeoutSecs"[\s\S]*?min="0"[\s\S]*?:max="MAX_QUERY_TIMEOUT_SECS"[\s\S]*?step="1"/);
+ expect(dialogSource).toMatch(/v-model\.number="form\.query_timeout_secs"[\s\S]*?min="0"[\s\S]*?:max="MAX_QUERY_TIMEOUT_SECS"[\s\S]*?step="1"/);
+ expect(dialogSource).not.toContain('v-model.number="editGlobalQueryTimeoutSecs" type="number" min="0" max="300"');
+ expect(dialogSource).not.toContain('v-model.number="form.query_timeout_secs" type="number" min="0" max="300"');
+ expect(dialogSource).toContain("@input=\"clampQueryTimeoutInput($event, 'global')\"");
+ expect(dialogSource).toContain("@input=\"clampQueryTimeoutInput($event, 'connection')\"");
+ expect(dialogSource).toContain("@input=\"clampConnectTimeoutInput($event, 'global')\"");
+ expect(dialogSource).toContain("@input=\"clampConnectTimeoutInput($event, 'connection')\"");
+ expect(dialogSource).not.toContain('@blur="editGlobalQueryTimeoutSecs = normalizeGlobalQueryTimeoutSecs');
+ });
+
+ it("restores the default when the connection timeout input is cleared", () => {
+ expect(normalizeConnectTimeoutSecs("")).toBe(DEFAULT_CONNECT_TIMEOUT_SECS);
+ expect(dialogSource).toContain("normalizeGlobalConnectTimeoutSecs(config.connect_timeout_secs)");
+ expect(dialogSource).not.toContain("Number(config.connect_timeout_secs)");
+ });
+
+ it("shows range help beside both global timeout labels", () => {
+ expect(dialogSource).toContain('t("connection.globalConnectTimeoutHint")');
+ expect(dialogSource).toContain('t("connection.globalQueryTimeoutHint")');
+ });
+});
diff --git a/apps/desktop/src/lib/__tests__/connection/timeoutInheritanceBackup.spec.ts b/apps/desktop/src/lib/__tests__/connection/timeoutInheritanceBackup.spec.ts
new file mode 100644
index 0000000000..774bb00412
--- /dev/null
+++ b/apps/desktop/src/lib/__tests__/connection/timeoutInheritanceBackup.spec.ts
@@ -0,0 +1,37 @@
+import { beforeEach, describe, expect, it } from "vitest";
+import { loadTimeoutInheritanceBackup, TIMEOUT_INHERITANCE_BACKUP_STORAGE_KEY } from "@/lib/connection/timeoutInheritanceBackup";
+
+const storage = new Map();
+Object.defineProperty(globalThis, "localStorage", {
+ configurable: true,
+ value: {
+ clear: () => storage.clear(),
+ getItem: (key: string) => storage.get(key) ?? null,
+ setItem: (key: string, value: string) => storage.set(key, value),
+ },
+});
+
+describe("timeout inheritance backup", () => {
+ beforeEach(() => localStorage.clear());
+
+ it("preserves query timeouts up to one hour while connection timeouts remain capped at 300 seconds", () => {
+ localStorage.setItem(
+ TIMEOUT_INHERITANCE_BACKUP_STORAGE_KEY,
+ JSON.stringify({
+ version: 1,
+ globalConnectTimeoutSecs: 301,
+ globalQueryTimeoutSecs: 3600,
+ connectSnapshots: { connection: 301 },
+ querySnapshots: { longQuery: 3600, tooLong: 3601, unlimited: 0 },
+ }),
+ );
+
+ expect(loadTimeoutInheritanceBackup()).toEqual({
+ version: 1,
+ globalConnectTimeoutSecs: 300,
+ globalQueryTimeoutSecs: 3600,
+ connectSnapshots: { connection: 300 },
+ querySnapshots: { longQuery: 3600, tooLong: 3600, unlimited: 0 },
+ });
+ });
+});
diff --git a/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts b/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts
index ccb9f66444..f11a9db4e9 100644
--- a/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts
+++ b/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts
@@ -1,3 +1,5 @@
+import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits";
+
export const TIMEOUT_INHERITANCE_BACKUP_STORAGE_KEY = "dbx-timeout-inheritance-backup-v1";
export interface TimeoutInheritanceBackup {
@@ -8,12 +10,12 @@ export interface TimeoutInheritanceBackup {
querySnapshots: Record;
}
-function normalizeSnapshots(value: unknown, min: number): Record {
+function normalizeSnapshots(value: unknown, normalize: (timeout: unknown) => number): Record {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const snapshots: Record = {};
for (const [id, timeout] of Object.entries(value)) {
if (!id.trim() || typeof timeout !== "number" || !Number.isFinite(timeout)) continue;
- snapshots[id] = Math.min(300, Math.max(min, Math.round(timeout)));
+ snapshots[id] = normalize(timeout);
}
return snapshots;
}
@@ -29,10 +31,10 @@ export function loadTimeoutInheritanceBackup(): TimeoutInheritanceBackup | null
if (!Number.isFinite(globalConnectTimeoutSecs) || !Number.isFinite(globalQueryTimeoutSecs)) return null;
return {
version: 1,
- globalConnectTimeoutSecs: Math.min(300, Math.max(1, Math.round(globalConnectTimeoutSecs))),
- globalQueryTimeoutSecs: Math.min(300, Math.max(0, Math.round(globalQueryTimeoutSecs))),
- connectSnapshots: normalizeSnapshots(parsed.connectSnapshots, 1),
- querySnapshots: normalizeSnapshots(parsed.querySnapshots, 0),
+ globalConnectTimeoutSecs: normalizeConnectTimeoutSecs(globalConnectTimeoutSecs),
+ globalQueryTimeoutSecs: normalizeQueryTimeoutSecs(globalQueryTimeoutSecs),
+ connectSnapshots: normalizeSnapshots(parsed.connectSnapshots, normalizeConnectTimeoutSecs),
+ querySnapshots: normalizeSnapshots(parsed.querySnapshots, normalizeQueryTimeoutSecs),
};
} catch {
return null;
diff --git a/apps/desktop/src/lib/connection/timeoutLimits.ts b/apps/desktop/src/lib/connection/timeoutLimits.ts
new file mode 100644
index 0000000000..3725d24928
--- /dev/null
+++ b/apps/desktop/src/lib/connection/timeoutLimits.ts
@@ -0,0 +1,14 @@
+export const DEFAULT_CONNECT_TIMEOUT_SECS = 10;
+export const MAX_CONNECT_TIMEOUT_SECS = 300;
+export const DEFAULT_QUERY_TIMEOUT_SECS = 30;
+export const MAX_QUERY_TIMEOUT_SECS = 3600;
+
+export function normalizeConnectTimeoutSecs(value: unknown): number {
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_CONNECT_TIMEOUT_SECS;
+ return Math.min(MAX_CONNECT_TIMEOUT_SECS, Math.max(1, Math.round(value)));
+}
+
+export function normalizeQueryTimeoutSecs(value: unknown): number {
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_QUERY_TIMEOUT_SECS;
+ return Math.min(MAX_QUERY_TIMEOUT_SECS, Math.max(0, Math.round(value)));
+}
diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
index b4be2964f3..20cabf1664 100644
--- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
+++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
@@ -238,7 +238,9 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({}).globalQueryTimeoutSecs).toBe(30);
expect(normalizeEditorSettings({ queryTimeoutSecs: 45 } as any).globalQueryTimeoutSecs).toBe(45);
expect(normalizeEditorSettings({ globalQueryTimeoutSecs: -1 }).globalQueryTimeoutSecs).toBe(0);
- expect(normalizeEditorSettings({ globalQueryTimeoutSecs: 301 }).globalQueryTimeoutSecs).toBe(300);
+ expect(normalizeEditorSettings({ globalQueryTimeoutSecs: 301 }).globalQueryTimeoutSecs).toBe(301);
+ expect(normalizeEditorSettings({ globalQueryTimeoutSecs: 3600 }).globalQueryTimeoutSecs).toBe(3600);
+ expect(normalizeEditorSettings({ globalQueryTimeoutSecs: 3601 }).globalQueryTimeoutSecs).toBe(3600);
expect(normalizeEditorSettings({ connectTimeoutInheritConnectionIds: ["one", "one", " ", "two"] }).connectTimeoutInheritConnectionIds).toEqual(["one", "two"]);
expect(normalizeEditorSettings({ queryTimeoutInheritConnectionIds: ["one", "one", " ", "two"] }).queryTimeoutInheritConnectionIds).toEqual(["one", "two"]);
expect(normalizeEditorSettings({}).timeoutInheritanceMigrationVersion).toBe(0);
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index dc1b52f8a6..5d36ce6cb8 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -9,6 +9,7 @@ import { type ColumnFormatterConfig, type CustomColumnFormatterConfig, normalize
import { type DataGridCopyPreference, type DataGridExtractorOptions, DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
+import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits";
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
@@ -800,13 +801,11 @@ const MIN_UI_SCALE = 0.75;
const MAX_UI_SCALE = 2;
export function normalizeGlobalQueryTimeoutSecs(value: unknown): number {
- if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs;
- return Math.min(300, Math.max(0, Math.round(value)));
+ return normalizeQueryTimeoutSecs(value);
}
export function normalizeGlobalConnectTimeoutSecs(value: unknown): number {
- if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs;
- return Math.min(300, Math.max(1, Math.round(value)));
+ return normalizeConnectTimeoutSecs(value);
}
function normalizeUiScale(value: unknown): number {