diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 11b874b594..b6ec78277d 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -74,7 +74,7 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { isWindows } from "@/lib/backend/platform"; import { applyMeilisearchBasePathToExternalConfig, applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl"; import { hasXuguConnectionDatabase } from "@/lib/connection/xuguDatabase"; -import { DEFAULT_QUERY_TIMEOUT_SECS, MAX_CONNECT_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits"; +import { DEFAULT_QUERY_TIMEOUT_SECS, MAX_CONNECT_TIMEOUT_SECS, MAX_IDLE_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS, supportsIdleTimeout } from "@/lib/connection/timeoutLimits"; import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection"; import { applyConnectionDeepLinkUpdate, resolveConnectionDeepLinkUpdate } from "@/lib/connection/connectionDeepLinkUpdate"; import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseConnectionDeepLinkUpdate, parseServiceConnectionUrl, type ConnectionDeepLinkDraft, type ConnectionDeepLinkUpdate } from "@/lib/connection/connectionDeepLink"; @@ -3848,6 +3848,7 @@ const showGenericUrlParamsHint = computed(() => form.value.db_type === "mysql" | const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]); const supportsMysqlTlsOptions = computed(() => mysqlTlsOptionsSupported(form.value.db_type, selectedType.value)); const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)); +const supportsIdleTimeoutSetting = computed(() => supportsIdleTimeout(form.value.db_type)); const supportsDoltSystemTables = computed(() => isDoltDriverProfile(form.value.driver_profile)); const showDoltSystemTables = computed({ get: () => doltSystemTablesVisible(form.value), @@ -6590,6 +6591,15 @@ function clampConnectTimeoutInput(event: Event, target: "global" | "connection") else form.value.connect_timeout_secs = MAX_CONNECT_TIMEOUT_SECS; } +function clampIdleTimeoutInput(event: Event) { + const input = event.target as HTMLInputElement; + if (input.value === "") return; + const value = Number(input.value); + if (!Number.isFinite(value) || value <= MAX_IDLE_TIMEOUT_SECS) return; + input.value = String(MAX_IDLE_TIMEOUT_SECS); + form.value.idle_timeout_secs = MAX_IDLE_TIMEOUT_SECS; +} + async function persistGlobalTimeoutDrafts() { const nextConnect = normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value); const nextQuery = normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value); @@ -10279,9 +10289,12 @@ function openExternalUrl(url: string) { -
- - +
+ +
+ +

{{ t("connection.idleTimeoutHint") }}

+
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index f2908f82f1..ae1493543a 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -1286,6 +1286,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Aralıq: 0–3600 saniyə. Vaxt limitini ləğv etmək üçün 0 təyin edin.", useConnectionQueryTimeout: "Əlaqə", idleTimeout: "Boşdayanma vaxt limiti (saniyə)", + idleTimeoutHint: "Bağlantı hovuzundakı boşda qalan bağlantıların geri qaytarılmasına tətbiq edilir.", keepaliveInterval: "Əlaqənin canlı saxlanma intervalı (saniyə)", readOnly: "Yalnız oxuma", readOnlyHint: "Bütün yazma əməliyyatlarını blokla (INSERT, UPDATE, DELETE və s.)", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index bad6f7c4ac..451c6d5abe 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1631,6 +1631,7 @@ export default { globalQueryTimeoutHint: "Range: 0 to 3600 seconds. Set to 0 for no timeout.", useConnectionQueryTimeout: "Connection", idleTimeout: "Idle Timeout (seconds)", + idleTimeoutHint: "Applies to idle connection reclamation within the connection pool.", keepaliveInterval: "Keepalive Interval (seconds)", readOnly: "Read Only", readOnlyHint: "Block all write operations (INSERT, UPDATE, DELETE, etc.)", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index e3f985b6f9..40af272bb6 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1375,6 +1375,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Rango: de 0 a 3600 segundos. Establece 0 para no aplicar límite.", useConnectionQueryTimeout: "Conexión", idleTimeout: "Tiempo de espera inactivo (segundos)", + idleTimeoutHint: "Se aplica a la recuperación de conexiones inactivas dentro del grupo de conexiones.", keepaliveInterval: "Intervalo de keepalive (segundos)", readOnly: "Solo lectura", readOnlyHint: "Bloquear todas las operaciones de escritura (INSERT, UPDATE, DELETE, etc.)", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index d0f5dda6f8..3b75874fcb 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -1523,6 +1523,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Rentang: 0 hingga 3600 detik. Tetapkan ke 0 untuk tanpa timeout.", useConnectionQueryTimeout: "Koneksi", idleTimeout: "Timeout Idle (detik)", + idleTimeoutHint: "Berlaku untuk pemulihan koneksi idle dalam connection pool.", keepaliveInterval: "Interval Keepalive (detik)", readOnly: "Read Only", readOnlyHint: "Blokir semua operasi tulis (INSERT, UPDATE, DELETE, dll.)", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 4c8271b460..d866077d4f 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1297,6 +1297,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Intervallo: da 0 a 3600 secondi. Imposta 0 per nessun limite.", useConnectionQueryTimeout: "Connessione", idleTimeout: "Timeout Inattività (secondi)", + idleTimeoutHint: "Si applica al recupero delle connessioni inattive all'interno del pool di connessioni.", keepaliveInterval: "Intervallo keepalive (secondi)", readOnly: "Sola lettura", readOnlyHint: "Blocca tutte le operazioni di scrittura (INSERT, UPDATE, DELETE, ecc.)", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index b6bdf4bf9b..535ff18ef5 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1267,6 +1267,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "0~3600 秒の範囲で指定します。0 はタイムアウトなしです。", useConnectionQueryTimeout: "接続", idleTimeout: "アイドルタイムアウト(秒)", + idleTimeoutHint: "コネクションプール内のアイドル接続の回収・解放に適用されます。", keepaliveInterval: "Keepalive 間隔(秒)", readOnly: "読み取り専用", readOnlyHint: "すべての書き込み操作(INSERT, UPDATE, DELETEなど)をブロックします", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index b8048f5cfc..155ab95c91 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1452,6 +1452,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "범위: 0~3600초. 0으로 설정하면 제한이 없습니다.", useConnectionQueryTimeout: "연결", idleTimeout: "유휴 제한 시간 (초)", + idleTimeoutHint: "연결 풀 내 유휴 연결의 회수 및 해제에 적용됩니다.", keepaliveInterval: "킵얼라이브 간격 (초)", readOnly: "읽기 전용", readOnlyHint: "모든 쓰기 작업 차단 (INSERT, UPDATE, DELETE 등)", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 9a2e04527d..f8ba4a0b56 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1298,6 +1298,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Intervalo: de 0 a 3600 segundos. Defina 0 para não haver limite.", useConnectionQueryTimeout: "Conexão", idleTimeout: "Timeout de Inatividade (segundos)", + idleTimeoutHint: "Aplica-se à recuperação de conexões inativas no pool de conexões.", keepaliveInterval: "Intervalo de keepalive (segundos)", readOnly: "Somente leitura", readOnlyHint: "Bloquear todas as operações de escrita (INSERT, UPDATE, DELETE, etc.)", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index e247bd74b0..32bd07e657 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -1542,6 +1542,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Диапазон: от 0 до 3600 секунд. Установите 0, чтобы отключить тайм-аут.", useConnectionQueryTimeout: "Подключение", idleTimeout: "Тайм-аут простоя (секунды)", + idleTimeoutHint: "Применяется для освобождения простаивающих соединений в пуле соединений.", keepaliveInterval: "Интервал keepalive (секунды)", readOnly: "Только чтение", readOnlyHint: "Блокировать все операции записи (INSERT, UPDATE, DELETE и т. д.)", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index a763d0d5e0..133d4d0807 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -1286,6 +1286,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "Aralık: 0 ile 3600 saniye. Zaman aşımı istemiyorsanız 0 yapın.", useConnectionQueryTimeout: "Bağlantı", idleTimeout: "Boşta Kalma Zaman Aşımı (saniye)", + idleTimeoutHint: "Bağlantı havuzundaki boşta kalan bağlantıların geri kazanılması için geçerlidir.", keepaliveInterval: "Keepalive Aralığı (saniye)", readOnly: "Salt Okunur", readOnlyHint: "Tüm yazma işlemlerini engelle (INSERT, UPDATE, DELETE vb.)", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 10be6c64e1..51378b58b1 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1570,6 +1570,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "范围为0~3600秒。设为0表示不限制", useConnectionQueryTimeout: "当前连接", idleTimeout: "空闲超时(秒)", + idleTimeoutHint: "作用于连接池内空闲连接的回收释放。", keepaliveInterval: "保持连接间隔(秒)", readOnly: "只读模式", readOnlyHint: "阻止所有写操作(INSERT、UPDATE、DELETE 等)", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 24e3975dec..0c81eb110e 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1309,6 +1309,7 @@ export default withEnglishFallback({ globalQueryTimeoutHint: "範圍為0~3600秒。設為0表示不限制", useConnectionQueryTimeout: "目前連線", idleTimeout: "閒置逾時(秒)", + idleTimeoutHint: "作用於連線池內閒置連線的回收釋放。", keepaliveInterval: "保持連線間隔(秒)", readOnly: "唯讀模式", readOnlyHint: "阻止所有寫入操作(INSERT、UPDATE、DELETE 等)", diff --git a/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts index c2710e5adc..798c7165ad 100644 --- a/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts +++ b/apps/desktop/src/lib/__tests__/connection/connectionDialogTimeouts.spec.ts @@ -1,8 +1,42 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { DEFAULT_CONNECT_TIMEOUT_SECS, normalizeConnectTimeoutSecs } from "@/lib/connection/timeoutLimits"; +import { DEFAULT_CONNECT_TIMEOUT_SECS, DEFAULT_IDLE_TIMEOUT_SECS, MAX_IDLE_TIMEOUT_SECS, normalizeConnectTimeoutSecs, normalizeIdleTimeoutSecs, supportsIdleTimeout } from "@/lib/connection/timeoutLimits"; + +const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8"); describe("ConnectionDialog timeout controls", () => { it("restores the default when the connection timeout input is cleared", () => { expect(normalizeConnectTimeoutSecs("")).toBe(DEFAULT_CONNECT_TIMEOUT_SECS); }); + + it("normalizes and bounds idle timeout values", () => { + expect(normalizeIdleTimeoutSecs("")).toBe(DEFAULT_IDLE_TIMEOUT_SECS); + expect(normalizeIdleTimeoutSecs(Number.NaN)).toBe(DEFAULT_IDLE_TIMEOUT_SECS); + expect(normalizeIdleTimeoutSecs(-1)).toBe(DEFAULT_IDLE_TIMEOUT_SECS); + expect(normalizeIdleTimeoutSecs(0)).toBe(0); + expect(normalizeIdleTimeoutSecs(60)).toBe(60); + expect(normalizeIdleTimeoutSecs(120.4)).toBe(120); + expect(normalizeIdleTimeoutSecs(5000)).toBe(MAX_IDLE_TIMEOUT_SECS); + }); + + it("identifies database dialects that support idle connection timeouts", () => { + expect(supportsIdleTimeout("mongodb")).toBe(true); + expect(supportsIdleTimeout("mysql")).toBe(true); + expect(supportsIdleTimeout("doris")).toBe(true); + expect(supportsIdleTimeout("starrocks")).toBe(true); + expect(supportsIdleTimeout("manticoresearch")).toBe(true); + + expect(supportsIdleTimeout("postgres")).toBe(false); + expect(supportsIdleTimeout("sqlite")).toBe(false); + expect(supportsIdleTimeout("redis")).toBe(false); + expect(supportsIdleTimeout("sqlserver")).toBe(false); + expect(supportsIdleTimeout("oracle")).toBe(false); + expect(supportsIdleTimeout(undefined)).toBe(false); + }); + + it("binds idle timeout setting and hint in ConnectionDialog", () => { + expect(dialogSource).toContain('v-show="supportsIdleTimeoutSetting"'); + expect(dialogSource).toContain("clampIdleTimeoutInput($event)"); + expect(dialogSource).toContain('t("connection.idleTimeoutHint")'); + }); }); diff --git a/apps/desktop/src/lib/connection/connectionSubmitNormalization.ts b/apps/desktop/src/lib/connection/connectionSubmitNormalization.ts index 48118f6aed..c2d6a9ffc6 100644 --- a/apps/desktop/src/lib/connection/connectionSubmitNormalization.ts +++ b/apps/desktop/src/lib/connection/connectionSubmitNormalization.ts @@ -1,3 +1,4 @@ +import { normalizeIdleTimeoutSecs } from "@/lib/connection/timeoutLimits"; import { isSingleDatabase } from "@/lib/database/databaseFeatureSupport"; import { normalizeGlobalConnectTimeoutSecs, normalizeGlobalQueryTimeoutSecs } from "@/stores/settingsStore"; import type { ConnectionConfig } from "@/types/database"; @@ -6,8 +7,7 @@ export function normalizeConnectionTimeouts(config: ConnectionConfig, connectTim config.connect_timeout_secs = config.connect_timeout_inherit === true ? normalizeGlobalConnectTimeoutSecs(connectTimeoutSecs) : normalizeGlobalConnectTimeoutSecs(config.connect_timeout_secs); const queryTimeout = Number(config.query_timeout_secs); config.query_timeout_secs = config.query_timeout_inherit === true ? normalizeGlobalQueryTimeoutSecs(queryTimeoutSecs) : normalizeGlobalQueryTimeoutSecs(queryTimeout); - const idleTimeout = Number(config.idle_timeout_secs); - config.idle_timeout_secs = Number.isFinite(idleTimeout) && idleTimeout >= 0 ? idleTimeout : 60; + config.idle_timeout_secs = normalizeIdleTimeoutSecs(config.idle_timeout_secs); const keepaliveInterval = Number(config.keepalive_interval_secs); config.keepalive_interval_secs = Number.isFinite(keepaliveInterval) && keepaliveInterval >= 0 ? keepaliveInterval : 30; } diff --git a/apps/desktop/src/lib/connection/timeoutLimits.ts b/apps/desktop/src/lib/connection/timeoutLimits.ts index 288d2cda27..f3c92e0f02 100644 --- a/apps/desktop/src/lib/connection/timeoutLimits.ts +++ b/apps/desktop/src/lib/connection/timeoutLimits.ts @@ -1,7 +1,11 @@ +import type { DatabaseType } from "@/types/database"; + export const DEFAULT_CONNECT_TIMEOUT_SECS = 10; export const MAX_CONNECT_TIMEOUT_SECS = 300; export const DEFAULT_QUERY_TIMEOUT_SECS = 60; export const MAX_QUERY_TIMEOUT_SECS = 3600; +export const DEFAULT_IDLE_TIMEOUT_SECS = 60; +export const MAX_IDLE_TIMEOUT_SECS = 3600; export function normalizeConnectTimeoutSecs(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_CONNECT_TIMEOUT_SECS; @@ -12,3 +16,12 @@ 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))); } + +export function normalizeIdleTimeoutSecs(value: unknown): number { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return DEFAULT_IDLE_TIMEOUT_SECS; + return Math.min(MAX_IDLE_TIMEOUT_SECS, Math.round(value)); +} + +export function supportsIdleTimeout(dbType?: DatabaseType): boolean { + return dbType === "mongodb" || dbType === "mysql" || dbType === "doris" || dbType === "starrocks" || dbType === "manticoresearch"; +}