diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 1186e4729..cd0cfef56 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -26,7 +26,7 @@ import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSum import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts"; import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync"; import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern"; -import { useSettingsStore } from "@/stores/settingsStore"; +import { normalizeGlobalConnectTimeoutSecs, normalizeGlobalQueryTimeoutSecs, useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import * as api from "@/lib/backend/api"; @@ -184,6 +184,8 @@ type ConnectionTestState = ConnectionTestResult & { ok: boolean }; const { t } = useI18n(); const { toast } = useToast(); const settingsStore = useSettingsStore(); +const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs); +const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs); const open = defineModel("open", { default: false }); const isDesktop = isTauriRuntime(); @@ -271,8 +273,10 @@ const defaultForm = (): ConnectionForm => ({ database: undefined, color: "", transport_layers: [], - connect_timeout_secs: 10, - query_timeout_secs: 30, + connect_timeout_secs: settingsStore.editorSettings.globalConnectTimeoutSecs, + connect_timeout_inherit: true, + query_timeout_secs: settingsStore.editorSettings.globalQueryTimeoutSecs, + query_timeout_inherit: true, idle_timeout_secs: 60, keepalive_interval_secs: 30, ssl: false, @@ -2291,6 +2295,8 @@ watch( ([config, isOpen]) => { const syncAction = connectionEditDraftSyncAction(config?.id ?? null, isOpen, editingId.value); if (syncAction === "preserve") return; + editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs; + editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs; if (syncAction === "hydrate" && config) { clearSavedDatabaseInfo(); const legacyConfig = config as LegacyConnectionConfig; @@ -2314,8 +2320,10 @@ watch( database: config.database, color: config.color || "", transport_layers: transportLayersForConfig(legacyConfig), - connect_timeout_secs: config.connect_timeout_secs || 10, - query_timeout_secs: config.query_timeout_secs ?? 30, + connect_timeout_secs: config.connect_timeout_inherit === true ? settingsStore.editorSettings.globalConnectTimeoutSecs : config.connect_timeout_secs || 10, + connect_timeout_inherit: config.connect_timeout_inherit === true, + query_timeout_secs: config.query_timeout_inherit === true ? settingsStore.editorSettings.globalQueryTimeoutSecs : (config.query_timeout_secs ?? 30), + query_timeout_inherit: config.query_timeout_inherit === true, idle_timeout_secs: config.idle_timeout_secs ?? 60, keepalive_interval_secs: config.keepalive_interval_secs ?? 30, ssl: config.ssl || false, @@ -3551,9 +3559,9 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo config.connection_string = undefined; } const connectTimeout = Number(config.connect_timeout_secs); - config.connect_timeout_secs = Number.isFinite(connectTimeout) && connectTimeout > 0 ? connectTimeout : 10; + config.connect_timeout_secs = config.connect_timeout_inherit === true ? normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value) : Number.isFinite(connectTimeout) && connectTimeout > 0 ? connectTimeout : 10; const queryTimeout = Number(config.query_timeout_secs); - config.query_timeout_secs = Number.isFinite(queryTimeout) && queryTimeout >= 0 ? queryTimeout : 30; + config.query_timeout_secs = config.query_timeout_inherit === true ? normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value) : Number.isFinite(queryTimeout) && queryTimeout >= 0 ? queryTimeout : 30; 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); @@ -4501,6 +4509,8 @@ function openJdbcDriverManagerFromError() { function resetForm() { editingId.value = null; form.value = defaultForm(); + editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs; + editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs; selectedTransportLayerId.value = null; draggedTransportLayerId.value = null; selectedType.value = "mysql"; @@ -4815,6 +4825,25 @@ function validateTransportLayers(config: LegacyConnectionConfig) { }); } +async function persistGlobalTimeoutDrafts() { + const nextConnect = normalizeGlobalConnectTimeoutSecs(editGlobalConnectTimeoutSecs.value); + const nextQuery = normalizeGlobalQueryTimeoutSecs(editGlobalQueryTimeoutSecs.value); + editGlobalConnectTimeoutSecs.value = nextConnect; + editGlobalQueryTimeoutSecs.value = nextQuery; + const connectChanged = nextConnect !== settingsStore.editorSettings.globalConnectTimeoutSecs; + const queryChanged = nextQuery !== settingsStore.editorSettings.globalQueryTimeoutSecs; + if (!connectChanged && !queryChanged) return; + settingsStore.updateEditorSettings({ + globalConnectTimeoutSecs: nextConnect, + globalQueryTimeoutSecs: nextQuery, + }); + await settingsStore.persistEditorSettings(); + await store.applyGlobalTimeouts({ + connectTimeoutSecs: connectChanged ? nextConnect : undefined, + queryTimeoutSecs: queryChanged ? nextQuery : undefined, + }); +} + async function save() { if (!ensureConnectionHostResolvedFromUrl()) return; if (isSaving.value) return; @@ -4825,12 +4854,14 @@ async function save() { const updated = withSavedDatabaseInfo(connectionConfigForSubmit(editingId.value), databaseInfoForSave); await ensureRequiredAgentDriverInstalled(updated); await ensureRequiredGaussdbMJdbcRuntime(updated); + await persistGlobalTimeoutDrafts(); await store.updateConnection(updated); store.stopEditing(); } else { const config = withSavedDatabaseInfo(connectionConfigForSubmit(draftTestConnectionId.value), databaseInfoForSave); await ensureRequiredAgentDriverInstalled(config); await ensureRequiredGaussdbMJdbcRuntime(config); + await persistGlobalTimeoutDrafts(); await store.addConnection(config); draftTestConnectionId.value = uuid(); if (config.db_type === "jdbc") { @@ -7402,11 +7433,33 @@ function openExternalUrl(url: string) {
- +
+
+ + + +
+
+ + + +
+
- +
+
+ + + +
+
+ + + +
+
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 6b2a388ca..fcd3ed133 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -272,6 +272,8 @@ const editCustomThemes = ref([...settingsStore.editorSettings.cus const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThemeId); const showThemeCustomizer = ref(false); const editExecuteMode = ref(settingsStore.editorSettings.executeMode); +const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs); +const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs); const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker); const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons); const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurrentStatementFrame); @@ -441,6 +443,8 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { customThemes: editCustomThemes.value, activeCustomThemeId: editActiveCustomThemeId.value, executeMode: editExecuteMode.value, + globalConnectTimeoutSecs: editGlobalConnectTimeoutSecs.value, + globalQueryTimeoutSecs: editGlobalQueryTimeoutSecs.value, showExecutionTargetPicker: editShowExecutionTargetPicker.value, showStatementRunButtons: editShowStatementRunButtons.value, showCurrentStatementFrame: editShowCurrentStatementFrame.value, @@ -709,6 +713,8 @@ function syncEditorSettingsDraftFromStore() { editCustomThemes.value = [...settingsStore.editorSettings.customThemes]; editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId; editExecuteMode.value = settingsStore.editorSettings.executeMode; + editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs; + editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs; editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker; editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons; editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame; @@ -860,6 +866,8 @@ function hasChanges(): boolean { async function persistSettings() { if (hasApplyBlocker.value) return; const editorSettingsPatch = editorSettingsPatchFromDraft(currentEditorSettingsDraft(), editEditorSettingsBase.value); + const globalConnectTimeoutChanged = editorSettingsPatch.globalConnectTimeoutSecs !== undefined; + const globalQueryTimeoutChanged = editorSettingsPatch.globalQueryTimeoutSecs !== undefined; const sidebarObjectDisplayChanged = editorSettingsPatch.sidebarObjectDisplay !== undefined && editorSettingsPatch.sidebarObjectDisplay !== settingsStore.editorSettings.sidebarObjectDisplay; const sidebarTablePageSizeChanged = editSidebarTablePageSize.value !== (settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE); if (Object.keys(editorSettingsPatch).length > 0) { @@ -867,6 +875,12 @@ async function persistSettings() { await settingsStore.persistEditorSettings(); editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings); } + if (globalConnectTimeoutChanged || globalQueryTimeoutChanged) { + await connectionStore.applyGlobalTimeouts({ + connectTimeoutSecs: globalConnectTimeoutChanged ? settingsStore.editorSettings.globalConnectTimeoutSecs : undefined, + queryTimeoutSecs: globalQueryTimeoutChanged ? settingsStore.editorSettings.globalQueryTimeoutSecs : undefined, + }); + } await settingsStore.updateDesktopSettings({ show_tray_icon: editShowTrayIcon.value, quit_on_close: editQuitOnClose.value, @@ -913,6 +927,8 @@ function resetDefaultsForTab(tab: SettingsCategory) { editFontFamily.value = DEFAULT_EDITOR_SETTINGS.fontFamily; editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize; editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; + editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs; + editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; @@ -1006,6 +1022,8 @@ function resetAllDefaults() { editCustomThemes.value = [...DEFAULT_EDITOR_SETTINGS.customThemes]; editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId; editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; + editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs; + editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; @@ -3628,6 +3646,26 @@ onUnmounted(() => {
+
+
+ +

+ {{ t("settings.globalConnectTimeoutDescription") }} +

+
+ +
+ +
+
+ +

+ {{ t("settings.globalQueryTimeoutDescription") }} +

+
+ +
+
diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index bf3da441b..101e8de3c 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2867,7 +2867,7 @@ async function lastPage() { const countTarget = await buildCurrentCountTarget(); const sql = countTarget?.sql; if (!sql) return; - const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId))); + const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId), settingsStore.editorSettings.globalQueryTimeoutSecs)); const total = Number(result.rows?.[0]?.[0] ?? 0); if (!Number.isFinite(total) || total < 0) return; manualTotalRowCount.value = total; @@ -2915,7 +2915,7 @@ async function calculateTotalRowCount() { if (!props.connectionId) return; const countTarget = await buildCurrentCountTarget(); if (!countTarget?.sql) return; - const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", countTarget.sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId))); + const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", countTarget.sql, countTarget.schema, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId), settingsStore.editorSettings.globalQueryTimeoutSecs)); const total = Number(result.rows?.[0]?.[0] ?? 0); if (Number.isFinite(total) && total >= 0) { manualTotalRowCount.value = total; diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 7d004d6fc..af9be4811 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -2317,7 +2317,7 @@ async function applyChanges() { try { const result = hasSqliteTypeChange.value ? await api.applySqliteTableStructureChange(props.connectionId, props.database, structureChangeOptions(), sqliteSchemaRevision.value!) - : await api.executeBatch(props.connectionId, props.database, pendingStatements.value, props.schema, queryTimeoutSecsForConnection(connection)); + : await api.executeBatch(props.connectionId, props.database, pendingStatements.value, props.schema, queryTimeoutSecsForConnection(connection, settingsStore.editorSettings.globalQueryTimeoutSecs)); await recordStructureHistory(sql, startedAt, true, result); if (!isCreateMode.value && props.tableName) { invalidateTableMetadataCache({ connectionId: props.connectionId, database: props.database, schema: metadataSchema.value, tableName: props.tableName }); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 35e2c231e..c74a458a3 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -785,6 +785,8 @@ export default { sshHopInvalidTimeout: "{hop}: SSH timeout must be between 1 and 300 seconds", connectTimeout: "Connection Timeout (seconds)", queryTimeout: "Query Timeout (seconds)", + useGlobalQueryTimeout: "Global", + useConnectionQueryTimeout: "Connection", idleTimeout: "Idle Timeout (seconds)", keepaliveInterval: "Keepalive Interval (seconds)", readOnly: "Read Only", @@ -4932,6 +4934,10 @@ export default { clearSettingsSearch: "Clear settings search", exitSettingsSearch: "Back to settings", editorTab: "Editor", + globalConnectTimeout: "Global connection timeout (seconds)", + globalConnectTimeoutDescription: "Used by connections that inherit the global setting.", + globalQueryTimeout: "Global query timeout (seconds)", + globalQueryTimeoutDescription: "Used by connections that inherit the global setting. Set to 0 for no timeout.", sqlFormatterTab: "SQL Formatter", sqlFormatterImport: "Import config", sqlFormatterExport: "Export config", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 2bc3e6edd..28874fc66 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -571,6 +571,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}: el tiempo de espera SSH debe estar entre 1 y 300 segundos", connectTimeout: "Tiempo de espera de conexión (segundos)", queryTimeout: "Tiempo de espera de consulta (segundos)", + useGlobalQueryTimeout: "Global", + useConnectionQueryTimeout: "Conexión", idleTimeout: "Tiempo de espera inactivo (segundos)", keepaliveInterval: "Intervalo de keepalive (segundos)", readOnly: "Solo lectura", @@ -4693,6 +4695,10 @@ export default withEnglishFallback({ clearSettingsSearch: "Borrar búsqueda de configuración", exitSettingsSearch: "Volver a configuración", editorTab: "Editor", + globalConnectTimeout: "Tiempo de conexión global (segundos)", + globalConnectTimeoutDescription: "Se usa en conexiones que heredan la configuración global.", + globalQueryTimeout: "Tiempo de espera global (segundos)", + globalQueryTimeoutDescription: "Se usa en conexiones que heredan la configuración global. Usa 0 para no limitar.", sqlFormatterTab: "Formateador SQL", sqlFormatterImport: "Importar configuración", sqlFormatterExport: "Exportar configuración", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 60faa728e..e8f0b4c96 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -569,6 +569,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}: Il timeout SSH deve essere compreso tra 1 e 300 secondi", connectTimeout: "Timeout Connessione (secondi)", queryTimeout: "Timeout Query (secondi)", + useGlobalQueryTimeout: "Globale", + useConnectionQueryTimeout: "Connessione", idleTimeout: "Timeout Inattività (secondi)", keepaliveInterval: "Intervallo keepalive (secondi)", readOnly: "Sola lettura", @@ -4693,6 +4695,10 @@ export default withEnglishFallback({ clearSettingsSearch: "Cancella ricerca impostazioni", exitSettingsSearch: "Torna alle impostazioni", editorTab: "Editor", + globalConnectTimeout: "Timeout globale connessione (secondi)", + globalConnectTimeoutDescription: "Usato dalle connessioni che ereditano l'impostazione globale.", + globalQueryTimeout: "Timeout query globale (secondi)", + globalQueryTimeoutDescription: "Usato dalle connessioni che ereditano l'impostazione globale. Usa 0 per nessun timeout.", sqlFormatterTab: "Formattatore SQL", sqlFormatterImport: "Importa configurazione", sqlFormatterExport: "Esporta configurazione", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index dea7f6e0d..e52b3bdf9 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -563,6 +563,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}: SSHタイムアウトは1〜300秒の範囲で指定してください", connectTimeout: "接続タイムアウト(秒)", queryTimeout: "クエリタイムアウト(秒)", + useGlobalQueryTimeout: "グローバル", + useConnectionQueryTimeout: "接続", idleTimeout: "アイドルタイムアウト(秒)", keepaliveInterval: "Keepalive 間隔(秒)", readOnly: "読み取り専用", @@ -4734,6 +4736,10 @@ export default withEnglishFallback({ clearSettingsSearch: "設定検索をクリア", exitSettingsSearch: "設定に戻る", editorTab: "エディタ", + globalConnectTimeout: "グローバル接続タイムアウト(秒)", + globalConnectTimeoutDescription: "グローバル設定を継承する接続で使用します。", + globalQueryTimeout: "グローバルクエリタイムアウト(秒)", + globalQueryTimeoutDescription: "グローバル設定を継承する接続で使用します。0 はタイムアウトなしです。", sqlFormatterTab: "SQLフォーマッター", sqlFormatterImport: "設定をインポート", sqlFormatterExport: "設定をエクスポート", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 355b47df4..3704e9520 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -683,6 +683,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}: SSH 제한 시간은 1에서 300초 사이여야 합니다", connectTimeout: "연결 제한 시간 (초)", queryTimeout: "쿼리 제한 시간 (초)", + useGlobalQueryTimeout: "전역", + useConnectionQueryTimeout: "연결", idleTimeout: "유휴 제한 시간 (초)", keepaliveInterval: "킵얼라이브 간격 (초)", readOnly: "읽기 전용", @@ -4448,6 +4450,10 @@ export default withEnglishFallback({ clearSettingsSearch: "설정 검색 지우기", exitSettingsSearch: "설정으로 돌아가기", editorTab: "편집기", + globalConnectTimeout: "전역 연결 제한 시간 (초)", + globalConnectTimeoutDescription: "전역 설정을 상속하는 연결에 사용됩니다.", + globalQueryTimeout: "전역 쿼리 제한 시간 (초)", + globalQueryTimeoutDescription: "전역 설정을 상속하는 연결에 사용됩니다. 0은 제한 시간 없음을 의미합니다.", sqlFormatterTab: "SQL 포매터", sqlFormatterImport: "설정 가져오기", sqlFormatterExport: "설정 내보내기", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 21f25bf87..5dc3871cd 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -570,6 +570,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}: o timeout SSH deve estar entre 1 e 300 segundos", connectTimeout: "Timeout de Conexão (segundos)", queryTimeout: "Timeout de Consulta (segundos)", + useGlobalQueryTimeout: "Global", + useConnectionQueryTimeout: "Conexão", idleTimeout: "Timeout de Inatividade (segundos)", keepaliveInterval: "Intervalo de keepalive (segundos)", readOnly: "Somente leitura", @@ -4695,6 +4697,10 @@ export default withEnglishFallback({ clearSettingsSearch: "Limpar pesquisa de configurações", exitSettingsSearch: "Voltar às configurações", editorTab: "Editor", + globalConnectTimeout: "Timeout global de conexão (segundos)", + globalConnectTimeoutDescription: "Usado por conexões que herdam a configuração global.", + globalQueryTimeout: "Timeout global de consulta (segundos)", + globalQueryTimeoutDescription: "Usado por conexões que herdam a configuração global. Use 0 para não limitar.", sqlFormatterTab: "Formatador SQL", sqlFormatterImport: "Importar configuração", sqlFormatterExport: "Exportar configuração", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 699fa2c84..6630fda8b 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -788,6 +788,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}:SSH 超时时间必须在 1 到 300 秒之间", connectTimeout: "连接超时(秒)", queryTimeout: "查询超时(秒)", + useGlobalQueryTimeout: "全局", + useConnectionQueryTimeout: "当前连接", idleTimeout: "空闲超时(秒)", keepaliveInterval: "保持连接间隔(秒)", readOnly: "只读模式", @@ -4929,6 +4931,10 @@ export default withEnglishFallback({ clearSettingsSearch: "清除设置搜索", exitSettingsSearch: "返回设置", editorTab: "编辑器", + globalConnectTimeout: "全局连接超时(秒)", + globalConnectTimeoutDescription: "供继承全局设置的连接使用。", + globalQueryTimeout: "全局查询超时(秒)", + globalQueryTimeoutDescription: "供继承全局设置的连接使用,设为 0 表示不限制。", sqlFormatterTab: "SQL 格式化", sqlFormatterImport: "导入配置", sqlFormatterExport: "导出配置", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index f34086162..aad488e95 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -569,6 +569,8 @@ export default withEnglishFallback({ sshHopInvalidTimeout: "{hop}:SSH 逾時時間必須在 1 到 300 秒之間", connectTimeout: "連線逾時(秒)", queryTimeout: "查詢逾時(秒)", + useGlobalQueryTimeout: "全域", + useConnectionQueryTimeout: "目前連線", idleTimeout: "閒置逾時(秒)", keepaliveInterval: "保持連線間隔(秒)", readOnly: "唯讀模式", @@ -4145,6 +4147,10 @@ export default withEnglishFallback({ clearSettingsSearch: "清除設定搜尋", exitSettingsSearch: "返回設定", editorTab: "編輯器", + globalConnectTimeout: "全域連線逾時(秒)", + globalConnectTimeoutDescription: "供繼承全域設定的連線使用。", + globalQueryTimeout: "全域查詢逾時(秒)", + globalQueryTimeoutDescription: "供繼承全域設定的連線使用,設為 0 表示不限制。", sqlFormatterTab: "SQL 格式化", sqlFormatterImport: "匯入設定", sqlFormatterExport: "匯出設定", diff --git a/apps/desktop/src/lib/__tests__/sql/queryTimeout.spec.ts b/apps/desktop/src/lib/__tests__/sql/queryTimeout.spec.ts index d19bf4464..c20380528 100644 --- a/apps/desktop/src/lib/__tests__/sql/queryTimeout.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/queryTimeout.spec.ts @@ -16,6 +16,16 @@ describe("queryTimeout", () => { it("keeps the existing frontend guard for other database types", () => { expect(frontendQueryTimeoutSecsForSql("SELECT * FROM sample_records LIMIT 2000", "mysql", 30)).toBe(60); - expect(queryTimeoutSecsForConnection({ query_timeout_secs: undefined })).toBe(60); + expect(queryTimeoutSecsForConnection({ query_timeout_secs: undefined })).toBe(30); + }); + + it("uses the global timeout only for inheriting connections", () => { + expect(queryTimeoutSecsForConnection({ query_timeout_secs: 30, query_timeout_inherit: true }, 12)).toBe(12); + expect(queryTimeoutSecsForConnection({ query_timeout_secs: 30, query_timeout_inherit: false }, 12)).toBe(30); + expect(queryTimeoutSecsForConnection({ query_timeout_secs: 0, query_timeout_inherit: false }, 12)).toBe(0); + }); + + it("falls back safely when an inherited global timeout is invalid", () => { + expect(queryTimeoutSecsForConnection({ query_timeout_inherit: true }, Number.NaN)).toBe(30); }); }); diff --git a/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts b/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts new file mode 100644 index 000000000..ccb9f6644 --- /dev/null +++ b/apps/desktop/src/lib/connection/timeoutInheritanceBackup.ts @@ -0,0 +1,48 @@ +export const TIMEOUT_INHERITANCE_BACKUP_STORAGE_KEY = "dbx-timeout-inheritance-backup-v1"; + +export interface TimeoutInheritanceBackup { + version: 1; + globalConnectTimeoutSecs: number; + globalQueryTimeoutSecs: number; + connectSnapshots: Record; + querySnapshots: Record; +} + +function normalizeSnapshots(value: unknown, min: 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))); + } + return snapshots; +} + +export function loadTimeoutInheritanceBackup(): TimeoutInheritanceBackup | null { + try { + const raw = localStorage.getItem(TIMEOUT_INHERITANCE_BACKUP_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + if (parsed.version !== 1) return null; + const globalConnectTimeoutSecs = Number(parsed.globalConnectTimeoutSecs); + const globalQueryTimeoutSecs = Number(parsed.globalQueryTimeoutSecs); + 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), + }; + } catch { + return null; + } +} + +export function saveTimeoutInheritanceBackup(backup: TimeoutInheritanceBackup): void { + try { + localStorage.setItem(TIMEOUT_INHERITANCE_BACKUP_STORAGE_KEY, JSON.stringify(backup)); + } catch { + // Settings persistence remains the primary source when local storage is unavailable. + } +} diff --git a/apps/desktop/src/lib/dataGrid/dataGridQueryOptions.ts b/apps/desktop/src/lib/dataGrid/dataGridQueryOptions.ts index fc43d9740..1a548a152 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridQueryOptions.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridQueryOptions.ts @@ -1,7 +1,10 @@ import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; import type { ConnectionConfig } from "@/types/database"; -export function dataGridCountQueryOptions(connection?: Pick | null): { +export function dataGridCountQueryOptions( + connection?: Pick | null, + globalQueryTimeoutSecs?: number, +): { maxRows: number; timeoutSecs: number; } { @@ -9,6 +12,6 @@ export function dataGridCountQueryOptions(connection?: Pick | null): number { +export function queryTimeoutSecsForConnection(connection?: Pick | null, globalQueryTimeoutSecs = DEFAULT_QUERY_TIMEOUT_SECS): number { + if (connection?.query_timeout_inherit === true) { + const globalValue = Number(globalQueryTimeoutSecs); + return Number.isFinite(globalValue) && globalValue >= 0 ? globalValue : DEFAULT_QUERY_TIMEOUT_SECS; + } const value = Number(connection?.query_timeout_secs); return Number.isFinite(value) && value >= 0 ? value : DEFAULT_QUERY_TIMEOUT_SECS; } diff --git a/apps/desktop/src/stores/__tests__/connectionStore.timeout.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.timeout.spec.ts index 50ec14e31..1912f7354 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.timeout.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.timeout.spec.ts @@ -82,6 +82,186 @@ describe("connectionStore timeout recovery", () => { expect(store.connections[0]?.keepalive_interval_secs).toBe(30); }); + it("migrates legacy timeout defaults to global inheritance and preserves custom overrides", async () => { + const saveConnections = vi.fn().mockResolvedValue(undefined); + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + loadConnections: vi + .fn() + .mockResolvedValue([postgresConnection({ id: "default", connect_timeout_secs: 10, query_timeout_secs: 30 }), postgresConnection({ id: "custom", connect_timeout_secs: 45, query_timeout_secs: 300 }), postgresConnection({ id: "inherited", connect_timeout_secs: 60, query_timeout_secs: 60 })]), + loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]), + loadSidebarLayout: vi.fn().mockResolvedValue(null), + loadTunnelProfiles: vi.fn().mockResolvedValue([]), + saveConnections, + saveEditorSettings: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const settingsStore = useSettingsStore(); + settingsStore.updateEditorSettings({ + globalConnectTimeoutSecs: 7, + connectTimeoutInheritConnectionIds: ["inherited"], + globalQueryTimeoutSecs: 12, + queryTimeoutInheritConnectionIds: ["inherited"], + }); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.initFromDisk(); + + expect(store.getConfig("default")).toMatchObject({ connect_timeout_secs: 7, connect_timeout_inherit: true, query_timeout_secs: 12, query_timeout_inherit: true }); + expect(store.getConfig("custom")).toMatchObject({ connect_timeout_secs: 45, connect_timeout_inherit: false, query_timeout_secs: 300, query_timeout_inherit: false }); + expect(store.getConfig("inherited")).toMatchObject({ connect_timeout_secs: 7, connect_timeout_inherit: true, query_timeout_secs: 12, query_timeout_inherit: true }); + expect(settingsStore.editorSettings.connectTimeoutInheritConnectionIds).toEqual(["default", "inherited"]); + expect(settingsStore.editorSettings.queryTimeoutInheritConnectionIds).toEqual(["default", "inherited"]); + expect(settingsStore.editorSettings.timeoutInheritanceMigrationVersion).toBe(2); + expect(saveConnections).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ id: "default", connect_timeout_secs: 7, query_timeout_secs: 12 }), + expect.objectContaining({ id: "custom", connect_timeout_secs: 45, query_timeout_secs: 300 }), + expect.objectContaining({ id: "inherited", connect_timeout_secs: 7, query_timeout_secs: 12 }), + ]), + ); + }); + + it("does not reclassify local default-valued overrides after migration", async () => { + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "local", connect_timeout_secs: 10, query_timeout_secs: 30 })]), + loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]), + loadSidebarLayout: vi.fn().mockResolvedValue(null), + loadTunnelProfiles: vi.fn().mockResolvedValue([]), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveEditorSettings: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const settingsStore = useSettingsStore(); + settingsStore.updateEditorSettings({ timeoutInheritanceMigrationVersion: 2 }); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.initFromDisk(); + + expect(store.getConfig("local")).toMatchObject({ connect_timeout_secs: 10, connect_timeout_inherit: false, query_timeout_secs: 30, query_timeout_inherit: false }); + }); + + it("preserves timeout inheritance across downgrade when snapshots are unchanged", async () => { + localStorage.setItem( + "dbx-timeout-inheritance-backup-v1", + JSON.stringify({ + version: 1, + globalConnectTimeoutSecs: 7, + globalQueryTimeoutSecs: 12, + connectSnapshots: { inherited: 7 }, + querySnapshots: { inherited: 12 }, + }), + ); + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "inherited", connect_timeout_secs: 7, query_timeout_secs: 12 })]), + loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]), + loadSidebarLayout: vi.fn().mockResolvedValue(null), + loadTunnelProfiles: vi.fn().mockResolvedValue([]), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveEditorSettings: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.initFromDisk(); + + expect(store.getConfig("inherited")).toMatchObject({ connect_timeout_secs: 7, connect_timeout_inherit: true, query_timeout_secs: 12, query_timeout_inherit: true }); + }); + + it("keeps timeout values changed by a downgraded version as local overrides", async () => { + localStorage.setItem( + "dbx-timeout-inheritance-backup-v1", + JSON.stringify({ + version: 1, + globalConnectTimeoutSecs: 7, + globalQueryTimeoutSecs: 12, + connectSnapshots: { inherited: 7 }, + querySnapshots: { inherited: 12 }, + }), + ); + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "inherited", connect_timeout_secs: 20, query_timeout_secs: 45 })]), + loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]), + loadSidebarLayout: vi.fn().mockResolvedValue(null), + loadTunnelProfiles: vi.fn().mockResolvedValue([]), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveEditorSettings: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const settingsStore = useSettingsStore(); + settingsStore.updateEditorSettings({ + globalConnectTimeoutSecs: 7, + connectTimeoutInheritConnectionIds: ["inherited"], + globalQueryTimeoutSecs: 12, + queryTimeoutInheritConnectionIds: ["inherited"], + timeoutInheritanceMigrationVersion: 2, + }); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.initFromDisk(); + + expect(store.getConfig("inherited")).toMatchObject({ connect_timeout_secs: 20, connect_timeout_inherit: false, query_timeout_secs: 45, query_timeout_inherit: false }); + expect(settingsStore.editorSettings.connectTimeoutInheritConnectionIds).toEqual([]); + expect(settingsStore.editorSettings.queryTimeoutInheritConnectionIds).toEqual([]); + }); + + it("exports effective timeout snapshots for older DBX versions", async () => { + const encryptConfig = vi.fn().mockResolvedValue({ encrypted: true }); + const click = vi.fn(); + const NativeUrl = globalThis.URL; + class TestUrl extends NativeUrl { + static createObjectURL = vi.fn(() => "blob:test"); + static revokeObjectURL = vi.fn(); + } + vi.stubGlobal("document", { createElement: vi.fn(() => ({ click, href: "", download: "" })) }); + vi.stubGlobal("URL", TestUrl); + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/configCrypto", () => ({ encryptConfig })); + vi.doMock("@/lib/backend/api", () => ({ + deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), + loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "inherited", connect_timeout_secs: 99, connect_timeout_inherit: true, query_timeout_secs: 99, query_timeout_inherit: true })]), + loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]), + loadSidebarLayout: vi.fn().mockResolvedValue(null), + loadTunnelProfiles: vi.fn().mockResolvedValue([]), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveEditorSettings: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const settingsStore = useSettingsStore(); + settingsStore.updateEditorSettings({ globalConnectTimeoutSecs: 7, globalQueryTimeoutSecs: 12 }); + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.initFromDisk(); + await store.exportConnectionsToFile("test-passphrase"); + + const exported = JSON.parse(encryptConfig.mock.calls[0]?.[0] as string); + expect(exported.connections[0]).toMatchObject({ + connect_timeout_secs: 7, + connect_timeout_inherit: true, + query_timeout_secs: 12, + query_timeout_inherit: true, + }); + expect(click).toHaveBeenCalledOnce(); + }); + it("clears connection node loading when health check timeout forces reconnect failure", async () => { const checkConnectionHealth = vi.fn(() => new Promise(() => undefined)); const connectDb = vi.fn().mockRejectedValue(new Error("reconnect failed")); diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index dc8af0506..b31296e86 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -220,6 +220,20 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ cellDetailMetadataCollapsed: true }).cellDetailMetadataCollapsed).toBe(true); }); + it("normalizes the global query timeout and inherited connection ids", () => { + expect(normalizeEditorSettings({}).globalConnectTimeoutSecs).toBe(10); + expect(normalizeEditorSettings({ globalConnectTimeoutSecs: 0 }).globalConnectTimeoutSecs).toBe(1); + 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({ connectTimeoutInheritConnectionIds: ["one", "one", " ", "two"] }).connectTimeoutInheritConnectionIds).toEqual(["one", "two"]); + expect(normalizeEditorSettings({ queryTimeoutInheritConnectionIds: ["one", "one", " ", "two"] }).queryTimeoutInheritConnectionIds).toEqual(["one", "two"]); + expect(normalizeEditorSettings({}).timeoutInheritanceMigrationVersion).toBe(0); + expect(normalizeEditorSettings({ queryTimeoutInheritanceMigrationVersion: 1 } as any).timeoutInheritanceMigrationVersion).toBe(1); + expect(normalizeEditorSettings({ timeoutInheritanceMigrationVersion: 2 }).timeoutInheritanceMigrationVersion).toBe(2); + }); + it("normalizes toolbar item settings from older saved settings", () => { const settings = normalizeEditorSettings({ toolbarItems: { diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 41bf6631e..1ea59ce83 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -71,6 +71,7 @@ import { findDatabaseTreeNode } from "@/lib/sidebar/treeRefreshTarget"; import { simpleModeEmptyShellNeedsConfirmedLoad, treeNodeLoadedChildrenContentPresent } from "@/lib/sidebar/treeLoadedChildrenMarker"; import { shouldMarkDisconnected } from "@/lib/connection/connectionHealth"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout"; +import { loadTimeoutInheritanceBackup, saveTimeoutInheritanceBackup } from "@/lib/connection/timeoutInheritanceBackup"; import { migrateSqlServerLegacyCompatibilityConfig, requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility"; import { deleteTabResultSnapshotsForOwner } from "@/lib/tabs/tabResultCache"; import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases"; @@ -975,6 +976,8 @@ export const useConnectionStore = defineStore("connection", () => { function normalizeConnection(config: ConnectionConfig): ConnectionConfig { config = { ...config }; migrateSqlServerLegacyCompatibilityConfig(config); + const connectTimeoutInherit = config.connect_timeout_inherit ?? settingsStore.editorSettings.connectTimeoutInheritConnectionIds.includes(config.id); + const queryTimeoutInherit = config.query_timeout_inherit ?? settingsStore.editorSettings.queryTimeoutInheritConnectionIds.includes(config.id); const labelMap: Record = { mysql: "MySQL", postgres: "PostgreSQL", @@ -1065,8 +1068,10 @@ export const useConnectionStore = defineStore("connection", () => { docs_notes_path: config.docs_notes_path?.trim() ? config.docs_notes_path.trim() : undefined, transport_layers: Array.isArray(config.transport_layers) ? config.transport_layers : [], show_system_schemas: config.show_system_schemas === true, - connect_timeout_secs: config.connect_timeout_secs || 10, - query_timeout_secs: config.query_timeout_secs ?? 30, + connect_timeout_secs: connectTimeoutInherit ? settingsStore.editorSettings.globalConnectTimeoutSecs : config.connect_timeout_secs || 10, + connect_timeout_inherit: connectTimeoutInherit, + query_timeout_secs: queryTimeoutInherit ? settingsStore.editorSettings.globalQueryTimeoutSecs : (config.query_timeout_secs ?? 30), + query_timeout_inherit: queryTimeoutInherit, idle_timeout_secs: config.idle_timeout_secs ?? 60, keepalive_interval_secs: config.keepalive_interval_secs ?? DEFAULT_KEEPALIVE_INTERVAL_SECS, redis_database_aliases: normalizeRedisDatabaseAliases(config.redis_database_aliases), @@ -2274,6 +2279,7 @@ export const useConnectionStore = defineStore("connection", () => { async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) { const normalized = normalizeConnection(config); + await persistTimeoutInheritance(normalized.id, normalized.connect_timeout_inherit === true, normalized.query_timeout_inherit === true); const existing = connections.value.findIndex((c) => c.id === normalized.id); const nextConnections = [...connections.value]; if (existing >= 0) { @@ -2285,6 +2291,7 @@ export const useConnectionStore = defineStore("connection", () => { } await persistConnections(nextConnections); connections.value = nextConnections; + syncTimeoutInheritanceBackup(); rebuildTreeNodes(); persistSidebarLayoutDebounced(); stopCreatingConnectionInGroup(); @@ -2386,7 +2393,12 @@ export const useConnectionStore = defineStore("connection", () => { const removedIds = new Set(connectionIds); const nextConnections = connections.value.filter((c) => !removedIds.has(c.id)); await persistConnections(nextConnections); + await persistTimeoutInheritanceIds( + settingsStore.editorSettings.connectTimeoutInheritConnectionIds.filter((id) => !removedIds.has(id)), + settingsStore.editorSettings.queryTimeoutInheritConnectionIds.filter((id) => !removedIds.has(id)), + ); connections.value = nextConnections; + syncTimeoutInheritanceBackup(); let nextPinnedOrder = pinnedTreeNodeOrder.value; for (const id of removedIds) { const prefix = `${id}:`; @@ -2431,8 +2443,10 @@ export const useConnectionStore = defineStore("connection", () => { const runtimeConfigChanged = connectionConfigFingerprint(connections.value[idx]) !== connectionConfigFingerprint(config); const nextConnections = [...connections.value]; nextConnections[idx] = config; + await persistTimeoutInheritance(config.id, config.connect_timeout_inherit === true, config.query_timeout_inherit === true); await persistConnections(nextConnections); connections.value = nextConnections; + syncTimeoutInheritanceBackup(); rebuildTreeNodes(); if (!runtimeConfigChanged) return; clearPrimaryVisibleObjectNames(config.id); @@ -6378,6 +6392,95 @@ export const useConnectionStore = defineStore("connection", () => { await api.saveConnections(nextConnections.filter((connection) => connection.one_time !== true)); } + function sameIds(left: string[], right: string[]) { + return left.length === right.length && left.every((id, index) => id === right[index]); + } + + async function persistTimeoutInheritanceIds(connectIds: string[], queryIds: string[]) { + if (sameIds(connectIds, settingsStore.editorSettings.connectTimeoutInheritConnectionIds) && sameIds(queryIds, settingsStore.editorSettings.queryTimeoutInheritConnectionIds)) return; + settingsStore.updateEditorSettings({ + connectTimeoutInheritConnectionIds: connectIds, + queryTimeoutInheritConnectionIds: queryIds, + }); + await settingsStore.persistEditorSettings(); + } + + async function persistTimeoutInheritance(connectionId: string, connectInherit: boolean, queryInherit: boolean) { + const connectIds = new Set(settingsStore.editorSettings.connectTimeoutInheritConnectionIds); + const queryIds = new Set(settingsStore.editorSettings.queryTimeoutInheritConnectionIds); + if (connectInherit) connectIds.add(connectionId); + else connectIds.delete(connectionId); + if (queryInherit) queryIds.add(connectionId); + else queryIds.delete(connectionId); + await persistTimeoutInheritanceIds([...connectIds], [...queryIds]); + } + + function syncTimeoutInheritanceBackup(source: ConnectionConfig[] = connections.value) { + const connectSnapshots: Record = {}; + const querySnapshots: Record = {}; + for (const connection of source) { + if (connection.connect_timeout_inherit === true) connectSnapshots[connection.id] = connection.connect_timeout_secs || settingsStore.editorSettings.globalConnectTimeoutSecs; + if (connection.query_timeout_inherit === true) querySnapshots[connection.id] = connection.query_timeout_secs ?? settingsStore.editorSettings.globalQueryTimeoutSecs; + } + saveTimeoutInheritanceBackup({ + version: 1, + globalConnectTimeoutSecs: settingsStore.editorSettings.globalConnectTimeoutSecs, + globalQueryTimeoutSecs: settingsStore.editorSettings.globalQueryTimeoutSecs, + connectSnapshots, + querySnapshots, + }); + } + + async function applyGlobalTimeouts({ connectTimeoutSecs, queryTimeoutSecs }: { connectTimeoutSecs?: number; queryTimeoutSecs?: number }) { + const nextConnections = connections.value.map((connection) => { + const nextConnectTimeout = connectTimeoutSecs !== undefined && connection.connect_timeout_inherit === true ? connectTimeoutSecs : connection.connect_timeout_secs; + const nextQueryTimeout = queryTimeoutSecs !== undefined && connection.query_timeout_inherit === true ? queryTimeoutSecs : connection.query_timeout_secs; + if (nextConnectTimeout === connection.connect_timeout_secs && nextQueryTimeout === connection.query_timeout_secs) return connection; + return { ...connection, connect_timeout_secs: nextConnectTimeout, query_timeout_secs: nextQueryTimeout }; + }); + if (nextConnections.some((connection, index) => connection !== connections.value[index])) { + await persistConnections(nextConnections); + connections.value = nextConnections; + } + syncTimeoutInheritanceBackup(); + } + + async function migrateTimeoutInheritance(saved: ConnectionConfig[]) { + const migrationVersion = settingsStore.editorSettings.timeoutInheritanceMigrationVersion; + const backup = loadTimeoutInheritanceBackup(); + const connectIdsBefore = new Set(settingsStore.editorSettings.connectTimeoutInheritConnectionIds); + const queryIdsBefore = new Set(settingsStore.editorSettings.queryTimeoutInheritConnectionIds); + const globalConnectTimeoutSecs = migrationVersion < 2 && backup ? backup.globalConnectTimeoutSecs : settingsStore.editorSettings.globalConnectTimeoutSecs; + const globalQueryTimeoutSecs = migrationVersion < 2 && backup ? backup.globalQueryTimeoutSecs : settingsStore.editorSettings.globalQueryTimeoutSecs; + + const resolveInheritance = (connection: ConnectionConfig, scope: "connect" | "query") => { + const explicit = scope === "connect" ? connection.connect_timeout_inherit : connection.query_timeout_inherit; + if (explicit === true || explicit === false) return explicit; + const ids = scope === "connect" ? connectIdsBefore : queryIdsBefore; + const snapshots = scope === "connect" ? backup?.connectSnapshots : backup?.querySnapshots; + const value = Number(scope === "connect" ? (connection.connect_timeout_secs ?? 10) : (connection.query_timeout_secs ?? 30)); + const snapshot = snapshots?.[connection.id]; + if (snapshot !== undefined && (ids.has(connection.id) || migrationVersion < 2)) return value === snapshot; + if (ids.has(connection.id)) return true; + if (scope === "connect" && migrationVersion < 2) return value === 10; + if (scope === "query" && migrationVersion < 1) return value === 30; + return false; + }; + + const connectIds = saved.filter((connection) => resolveInheritance(connection, "connect")).map((connection) => connection.id); + const queryIds = saved.filter((connection) => resolveInheritance(connection, "query")).map((connection) => connection.id); + settingsStore.updateEditorSettings({ + globalConnectTimeoutSecs, + connectTimeoutInheritConnectionIds: connectIds, + globalQueryTimeoutSecs, + queryTimeoutInheritConnectionIds: queryIds, + timeoutInheritanceMigrationVersion: 2, + }); + if (migrationVersion !== 2 || !sameIds(connectIds, [...connectIdsBefore]) || !sameIds(queryIds, [...queryIdsBefore])) { + await settingsStore.persistEditorSettings(); + } + } + function persistSidebarLayoutDebounced() { if (layoutPersistTimer) clearTimeout(layoutPersistTimer); layoutPersistTimer = setTimeout(() => { @@ -6506,7 +6609,14 @@ export const useConnectionStore = defineStore("connection", () => { const { encryptConfig } = await import("@/lib/backend/configCrypto"); const tunnelProfileStore = useTunnelProfileStore(); await tunnelProfileStore.init(); - const exportData = { connections: connections.value, layout: sidebarLayout.value, tunnelProfiles: tunnelProfileStore.profiles }; + // Older DBX versions ignore inheritance flags, so always include the + // effective numeric values as a backward-compatible snapshot. + const exportedConnections = connections.value.map((connection) => ({ + ...connection, + connect_timeout_secs: connection.connect_timeout_inherit === true ? settingsStore.editorSettings.globalConnectTimeoutSecs : connection.connect_timeout_secs, + query_timeout_secs: connection.query_timeout_inherit === true ? settingsStore.editorSettings.globalQueryTimeoutSecs : connection.query_timeout_secs, + })); + const exportData = { connections: exportedConnections, layout: sidebarLayout.value, tunnelProfiles: tunnelProfileStore.profiles }; const json = JSON.stringify(exportData); const payload = await encryptConfig(json, passphrase); const content = JSON.stringify(payload, null, 2); @@ -6886,7 +6996,12 @@ export const useConnectionStore = defineStore("connection", () => { initFromDiskPromise = (async () => { const [pinnedOrder, saved] = await Promise.all([loadPinnedTreeNodeOrder(), api.loadConnections(), tunnelProfileStore.init()]); setPinnedTreeNodeOrder(pinnedOrder); + await migrateTimeoutInheritance(saved); connections.value = saved.map(normalizeConnection); + if (connections.value.some((connection, index) => (connection.connect_timeout_inherit === true && connection.connect_timeout_secs !== saved[index]?.connect_timeout_secs) || (connection.query_timeout_inherit === true && connection.query_timeout_secs !== saved[index]?.query_timeout_secs))) { + await persistConnections(); + } + syncTimeoutInheritanceBackup(); const savedLayout = await api.loadSidebarLayout(); const currentLayout = sidebarLayout.value.groups.length || sidebarLayout.value.order.length ? sidebarLayout.value : null; sidebarLayout.value = reconcileLayout( @@ -6964,6 +7079,7 @@ export const useConnectionStore = defineStore("connection", () => { pasteConnectionClipboard, addEphemeralConnection, updateConnection, + applyGlobalTimeouts, updateConnectionDatabaseInfo, setDefaultDatabase, clearDefaultDatabase, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 1cc26addb..2c36c3e7e 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -3553,8 +3553,7 @@ export const useQueryStore = defineStore("query", () => { const effectiveDbType = effectiveDatabaseTypeForConnection(conn); const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog); const useAgentCursor = usesAgentCursorForQuery(conn?.db_type); - const queryTimeoutSecs = queryTimeoutSecsForConnection(conn); - const settingsStore = useSettingsStore(); + const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const statementExecution = tab.mode === "query" ? createBatchSqlExecution(executionId, tab.sql, sql, effectiveDbType, options?.sourceOffset) : undefined; tab.batchSqlExecution = statementExecution && (tab.autoCommit !== false || statementExecution.total === 1) ? statementExecution : undefined; if (tab.batchSqlExecution) liveBatchSqlExecutions.set(tab, tab.batchSqlExecution); @@ -4411,7 +4410,7 @@ export const useQueryStore = defineStore("query", () => { const tab = tabs.value.find((t) => t.id === id); if (!tab) return { ok: false as const, reason: "empty" as const }; const conn = useConnectionStore().getConfig(tab.connectionId); - const queryTimeoutSecs = queryTimeoutSecsForConnection(conn); + const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const executionId = uuid(); tab.isExplaining = true; @@ -5091,7 +5090,7 @@ export const useQueryStore = defineStore("query", () => { const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys; const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined; const orderBy = tab.orderByInput?.trim() || sortOrder; - const queryTimeoutSecs = queryTimeoutSecsForConnection(conn); + const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tableMeta.catalog); const rows: QueryResult["rows"] = []; let columns: string[] = []; @@ -5157,7 +5156,7 @@ export const useQueryStore = defineStore("query", () => { await connStore.ensureConnected(tab.connectionId); const conn = connStore.getConfig(tab.connectionId); const effectiveDbType = effectiveDatabaseTypeForConnection(conn); - const queryTimeoutSecs = queryTimeoutSecsForConnection(conn); + const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs); const useAgentCursor = usesAgentCursorForQuery(conn?.db_type); const queryBaseSql = queryResultBaseSql(tab); const exportSettings = useSettingsStore().editorSettings; @@ -5324,7 +5323,7 @@ export const useQueryStore = defineStore("query", () => { pageSize: settings.exportBatchSize, rowLimit, totalRows, - timeoutSecs: queryTimeoutSecsForConnection(conn), + timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs), keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled, clientSessionId, executionId: uuid(), @@ -5360,7 +5359,7 @@ export const useQueryStore = defineStore("query", () => { pageSize: settings.exportBatchSize, rowLimit: settings.exportRowLimitEnabled ? settings.exportRowLimit : null, totalRows: null, - timeoutSecs: queryTimeoutSecsForConnection(conn), + timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs), keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled, clientSessionId: `${tabClientSessionId(tab, "export")}:${exportId}`, executionId: uuid(), diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index fc7fb81f9..c154ab03c 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -466,6 +466,11 @@ export interface EditorSettings { activeCustomThemeId: string; executeMode: "all" | "current"; executeModeDefaultVersion: number; + globalConnectTimeoutSecs: number; + connectTimeoutInheritConnectionIds: string[]; + globalQueryTimeoutSecs: number; + queryTimeoutInheritConnectionIds: string[]; + timeoutInheritanceMigrationVersion: number; showExecutionTargetPicker: boolean; showStatementRunButtons: boolean; showCurrentStatementFrame: boolean; @@ -647,6 +652,11 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { activeCustomThemeId: "default", executeMode: "current", executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION, + globalConnectTimeoutSecs: 10, + connectTimeoutInheritConnectionIds: [], + globalQueryTimeoutSecs: 30, + queryTimeoutInheritConnectionIds: [], + timeoutInheritanceMigrationVersion: 2, showExecutionTargetPicker: false, showStatementRunButtons: true, showCurrentStatementFrame: true, @@ -742,6 +752,16 @@ const LEGACY_DEFAULT_EXPORT_BATCH_SIZE = 10000; 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))); +} + +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))); +} + function normalizeUiScale(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.uiScale; return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value * 100) / 100)); @@ -914,6 +934,7 @@ function normalizeTableInfoTab(value: unknown): TableInfoTab { } export function normalizeEditorSettings(settings: Partial, existing?: EditorSettings): EditorSettings { + const legacyTimeoutSettings = settings as Partial & { queryTimeoutSecs?: unknown; queryTimeoutInheritanceMigrationVersion?: unknown }; const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled); const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion; const executeModeDefaultVersion = typeof savedExecuteModeDefaultVersion === "number" && savedExecuteModeDefaultVersion >= EXECUTE_MODE_CURRENT_DEFAULT_VERSION ? savedExecuteModeDefaultVersion : EXECUTE_MODE_CURRENT_DEFAULT_VERSION; @@ -959,6 +980,16 @@ export function normalizeEditorSettings(settings: Partial, exist activeCustomThemeId: settings.activeCustomThemeId ?? "default", executeMode: hasCurrentExecuteModeDefault && (settings.executeMode === "all" || settings.executeMode === "current") ? settings.executeMode : DEFAULT_EDITOR_SETTINGS.executeMode, executeModeDefaultVersion, + globalConnectTimeoutSecs: normalizeGlobalConnectTimeoutSecs(settings.globalConnectTimeoutSecs), + connectTimeoutInheritConnectionIds: Array.isArray(settings.connectTimeoutInheritConnectionIds) ? [...new Set(settings.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [], + globalQueryTimeoutSecs: normalizeGlobalQueryTimeoutSecs(settings.globalQueryTimeoutSecs ?? legacyTimeoutSettings.queryTimeoutSecs), + queryTimeoutInheritConnectionIds: Array.isArray(settings.queryTimeoutInheritConnectionIds) ? [...new Set(settings.queryTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [], + timeoutInheritanceMigrationVersion: + typeof settings.timeoutInheritanceMigrationVersion === "number" && settings.timeoutInheritanceMigrationVersion >= 1 + ? Math.floor(settings.timeoutInheritanceMigrationVersion) + : typeof legacyTimeoutSettings.queryTimeoutInheritanceMigrationVersion === "number" && legacyTimeoutSettings.queryTimeoutInheritanceMigrationVersion >= 1 + ? 1 + : 0, showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker, showStatementRunButtons: typeof settings.showStatementRunButtons === "boolean" ? settings.showStatementRunButtons : DEFAULT_EDITOR_SETTINGS.showStatementRunButtons, showCurrentStatementFrame: typeof settings.showCurrentStatementFrame === "boolean" ? settings.showCurrentStatementFrame : DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame, @@ -1449,6 +1480,15 @@ export const useSettingsStore = defineStore("settings", () => { } } if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode; + if (partial.globalConnectTimeoutSecs !== undefined) editorSettings.value.globalConnectTimeoutSecs = normalizeGlobalConnectTimeoutSecs(partial.globalConnectTimeoutSecs); + if (partial.connectTimeoutInheritConnectionIds !== undefined) { + editorSettings.value.connectTimeoutInheritConnectionIds = [...new Set(partial.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))]; + } + if (partial.globalQueryTimeoutSecs !== undefined) editorSettings.value.globalQueryTimeoutSecs = normalizeGlobalQueryTimeoutSecs(partial.globalQueryTimeoutSecs); + if (partial.queryTimeoutInheritConnectionIds !== undefined) { + editorSettings.value.queryTimeoutInheritConnectionIds = [...new Set(partial.queryTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))]; + } + if (partial.timeoutInheritanceMigrationVersion !== undefined) editorSettings.value.timeoutInheritanceMigrationVersion = Math.max(0, Math.floor(partial.timeoutInheritanceMigrationVersion)); if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker; if (partial.showStatementRunButtons !== undefined) editorSettings.value.showStatementRunButtons = partial.showStatementRunButtons === true; if (partial.showCurrentStatementFrame !== undefined) editorSettings.value.showCurrentStatementFrame = partial.showCurrentStatementFrame === true; diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index bb5b036fb..70bff0e6f 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -152,7 +152,9 @@ export interface ConnectionConfig { docs_notes_path?: string; transport_layers?: TransportLayerConfig[]; connect_timeout_secs?: number; + connect_timeout_inherit?: boolean; query_timeout_secs?: number; + query_timeout_inherit?: boolean; idle_timeout_secs?: number; keepalive_interval_secs?: number; ssl?: boolean; diff --git a/packages/app-tests/dataGridQueryOptions.test.ts b/packages/app-tests/dataGridQueryOptions.test.ts index 1922c4013..68fbcd2cc 100644 --- a/packages/app-tests/dataGridQueryOptions.test.ts +++ b/packages/app-tests/dataGridQueryOptions.test.ts @@ -19,6 +19,6 @@ test("count queries preserve disabled timeouts", () => { test("count queries use the frontend default for missing configurations", () => { assert.deepEqual(dataGridCountQueryOptions(undefined), { maxRows: 1, - timeoutSecs: 60, + timeoutSecs: 30, }); }); diff --git a/packages/app-tests/queryTimeout.test.ts b/packages/app-tests/queryTimeout.test.ts index 83b692661..6be396ad9 100644 --- a/packages/app-tests/queryTimeout.test.ts +++ b/packages/app-tests/queryTimeout.test.ts @@ -3,7 +3,7 @@ import { test } from "vitest"; import { DEFAULT_QUERY_TIMEOUT_SECS, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "../../apps/desktop/src/lib/sql/queryTimeout.ts"; test("queryTimeoutSecsForConnection falls back to the default timeout", () => { - assert.equal(DEFAULT_QUERY_TIMEOUT_SECS, 60); + assert.equal(DEFAULT_QUERY_TIMEOUT_SECS, 30); assert.equal(queryTimeoutSecsForConnection(undefined), DEFAULT_QUERY_TIMEOUT_SECS); assert.equal(queryTimeoutSecsForConnection({ query_timeout_secs: -1 }), DEFAULT_QUERY_TIMEOUT_SECS); assert.equal(queryTimeoutSecsForConnection({ query_timeout_secs: 0 }), 0);