feat(timeout): support global connection and query timeout settings

This commit is contained in:
zipg 2026-08-09 13:57:42 +08:00 committed by GitHub
parent 5c343ab416
commit 72ff141f02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 588 additions and 29 deletions

View File

@ -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<boolean>("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) {
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.connectTimeout") }}</Label>
<Input v-model.number="form.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-3" />
<div class="col-span-3 grid grid-cols-2 gap-2">
<div class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-1 rounded border px-2 py-1.5 sm:flex" :class="form.connect_timeout_inherit === true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
<input id="connect-timeout-global" v-model="form.connect_timeout_inherit" type="radio" name="connect-timeout-scope" :value="true" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="connect-timeout-global" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useGlobalQueryTimeout')">{{ t("connection.useGlobalQueryTimeout") }}</label>
<Input v-model.number="editGlobalConnectTimeoutSecs" type="number" min="1" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.connect_timeout_inherit !== true" />
</div>
<div class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-1 rounded border px-2 py-1.5 sm:flex" :class="form.connect_timeout_inherit !== true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
<input id="connect-timeout-connection" v-model="form.connect_timeout_inherit" type="radio" name="connect-timeout-scope" :value="false" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="connect-timeout-connection" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useConnectionQueryTimeout')">{{ t("connection.useConnectionQueryTimeout") }}</label>
<Input v-model.number="form.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.connect_timeout_inherit === true" />
</div>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.queryTimeout") }}</Label>
<Input v-model.number="form.query_timeout_secs" type="number" min="0" max="300" step="1" class="col-span-3" />
<div class="col-span-3 grid grid-cols-2 gap-2">
<div class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-1 rounded border px-2 py-1.5 sm:flex" :class="form.query_timeout_inherit === true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
<input id="query-timeout-global" v-model="form.query_timeout_inherit" type="radio" name="query-timeout-scope" :value="true" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="query-timeout-global" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useGlobalQueryTimeout')">{{ t("connection.useGlobalQueryTimeout") }}</label>
<Input v-model.number="editGlobalQueryTimeoutSecs" type="number" min="0" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.query_timeout_inherit !== true" />
</div>
<div class="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-1 rounded border px-2 py-1.5 sm:flex" :class="form.query_timeout_inherit !== true ? 'border-primary/60 bg-background' : 'border-border bg-muted/30 text-muted-foreground'">
<input id="query-timeout-connection" v-model="form.query_timeout_inherit" type="radio" name="query-timeout-scope" :value="false" class="h-3.5 w-3.5 shrink-0 accent-primary" />
<label for="query-timeout-connection" class="min-w-0 flex-1 cursor-pointer truncate text-xs" :title="t('connection.useConnectionQueryTimeout')">{{ t("connection.useConnectionQueryTimeout") }}</label>
<Input v-model.number="form.query_timeout_secs" type="number" min="0" max="300" step="1" class="col-span-2 h-7 w-full shrink-0 sm:col-span-1 sm:w-20" :disabled="form.query_timeout_inherit === true" />
</div>
</div>
</div>
<div v-show="form.db_type === 'mongodb'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.idleTimeout") }}</Label>

View File

@ -272,6 +272,8 @@ const editCustomThemes = ref<CustomTheme[]>([...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(() => {
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-global-connect-timeout">{{ t("settings.globalConnectTimeout") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.globalConnectTimeoutDescription") }}
</p>
</div>
<Input id="editor-global-connect-timeout" v-model.number="editGlobalConnectTimeoutSecs" type="number" min="1" max="300" step="1" class="w-24" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-global-query-timeout">{{ t("settings.globalQueryTimeout") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.globalQueryTimeoutDescription") }}
</p>
</div>
<Input id="editor-global-query-timeout" v-model.number="editGlobalQueryTimeoutSecs" type="number" min="0" max="300" step="1" class="w-24" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-show-execution-target-picker">{{ t("settings.showExecutionTargetPicker") }}</Label>

View File

@ -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;

View File

@ -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 });

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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: "設定をエクスポート",

View File

@ -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: "설정 내보내기",

View File

@ -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",

View File

@ -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: "导出配置",

View File

@ -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: "匯出設定",

View File

@ -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);
});
});

View File

@ -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<string, number>;
querySnapshots: Record<string, number>;
}
function normalizeSnapshots(value: unknown, min: number): Record<string, number> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const snapshots: Record<string, number> = {};
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<TimeoutInheritanceBackup>;
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.
}
}

View File

@ -1,7 +1,10 @@
import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
import type { ConnectionConfig } from "@/types/database";
export function dataGridCountQueryOptions(connection?: Pick<ConnectionConfig, "query_timeout_secs"> | null): {
export function dataGridCountQueryOptions(
connection?: Pick<ConnectionConfig, "query_timeout_secs" | "query_timeout_inherit"> | null,
globalQueryTimeoutSecs?: number,
): {
maxRows: number;
timeoutSecs: number;
} {
@ -9,6 +12,6 @@ export function dataGridCountQueryOptions(connection?: Pick<ConnectionConfig, "q
// inherit the connection setting instead of falling back to the backend's shorter legacy default.
return {
maxRows: 1,
timeoutSecs: queryTimeoutSecsForConnection(connection),
timeoutSecs: queryTimeoutSecsForConnection(connection, globalQueryTimeoutSecs),
};
}

View File

@ -12,6 +12,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"customThemes",
"activeCustomThemeId",
"executeMode",
"globalConnectTimeoutSecs",
"globalQueryTimeoutSecs",
"showExecutionTargetPicker",
"showStatementRunButtons",
"showCurrentStatementFrame",

View File

@ -106,6 +106,8 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-theme", category: "editor", titleKey: "settings.theme", targetId: "editor" },
{ id: "editor-font-size", category: "editor", titleKey: "settings.fontSize", targetId: "editor" },
{ id: "editor-execute-mode", category: "editor", titleKey: "settings.executeMode", targetId: "editor" },
{ id: "editor-global-connect-timeout", category: "editor", titleKey: "settings.globalConnectTimeout", descriptionKey: "settings.globalConnectTimeoutDescription", targetId: "editor" },
{ id: "editor-global-query-timeout", category: "editor", titleKey: "settings.globalQueryTimeout", descriptionKey: "settings.globalQueryTimeoutDescription", targetId: "editor" },
{ id: "editor-execution-target", category: "editor", titleKey: "settings.showExecutionTargetPicker", descriptionKey: "settings.showExecutionTargetPickerDescription", targetId: "editor" },
{ id: "editor-run-buttons", category: "editor", titleKey: "settings.showStatementRunButtons", descriptionKey: "settings.showStatementRunButtonsDescription", targetId: "editor" },
{ id: "editor-statement-frame", category: "editor", titleKey: "settings.showCurrentStatementFrame", descriptionKey: "settings.showCurrentStatementFrameDescription", targetId: "editor" },

View File

@ -2,12 +2,16 @@ import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
import type { ConnectionConfig, DatabaseType } from "@/types/database";
export const DEFAULT_QUERY_TIMEOUT_SECS = 60;
export const DEFAULT_QUERY_TIMEOUT_SECS = 30;
const POSTGRES_ROW_STATEMENT_KEYWORDS = new Set(["select", "show", "explain", "table", "with"]);
const POSTGRES_RETURNING_STATEMENT_KEYWORDS = new Set(["insert", "update", "delete", "merge"]);
export function queryTimeoutSecsForConnection(connection?: Pick<ConnectionConfig, "query_timeout_secs"> | null): number {
export function queryTimeoutSecsForConnection(connection?: Pick<ConnectionConfig, "query_timeout_secs" | "query_timeout_inherit"> | 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;
}

View File

@ -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"));

View File

@ -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: {

View File

@ -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<string, string> = {
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<string, number> = {};
const querySnapshots: Record<string, number> = {};
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,

View File

@ -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(),

View File

@ -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<EditorSettings>, existing?: EditorSettings): EditorSettings {
const legacyTimeoutSettings = settings as Partial<EditorSettings> & { 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<EditorSettings>, 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;

View File

@ -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;

View File

@ -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,
});
});

View File

@ -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);