feat(timeout): support global connection and query timeout settings
This commit is contained in:
parent
5c343ab416
commit
72ff141f02
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "設定をエクスポート",
|
||||
|
|
|
|||
|
|
@ -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: "설정 내보내기",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "导出配置",
|
||||
|
|
|
|||
|
|
@ -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: "匯出設定",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
|||
"customThemes",
|
||||
"activeCustomThemeId",
|
||||
"executeMode",
|
||||
"globalConnectTimeoutSecs",
|
||||
"globalQueryTimeoutSecs",
|
||||
"showExecutionTargetPicker",
|
||||
"showStatementRunButtons",
|
||||
"showCurrentStatementFrame",
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue