From 792c816a171d6948fd44f2e16bf018e10f8ec05b Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Mon, 25 May 2026 18:30:55 +0800
Subject: [PATCH] feat: make query timeout configurable via global settings
(default 30s)
Add queryTimeoutSecs to editor settings with UI control in the settings
dialog. 0 means no timeout. Applied to all query execution paths including
sidebar table data loading and EXPLAIN queries.
Closes #441.
---
.../editor/EditorSettingsDialog.vue | 25 ++++++
apps/desktop/src/i18n/locales/en.ts | 2 +
apps/desktop/src/i18n/locales/zh-CN.ts | 2 +
apps/desktop/src/lib/http.ts | 2 +
apps/desktop/src/lib/tauri.ts | 2 +
apps/desktop/src/stores/queryStore.ts | 13 +--
apps/desktop/src/stores/settingsStore.ts | 12 +++
crates/dbx-core/src/query.rs | 80 ++++++++++++++++---
crates/dbx-web/src/routes/query.rs | 3 +
src-tauri/src/commands/query.rs | 4 +
10 files changed, 127 insertions(+), 18 deletions(-)
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index 9dc60f3a7..36f91b449 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -66,6 +66,7 @@ const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
+const editQueryTimeoutSecs = ref(settingsStore.editorSettings.queryTimeoutSecs);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation);
const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSelectActiveSidebarNode);
@@ -198,6 +199,7 @@ watch(
editAppLayout.value = settingsStore.editorSettings.appLayout;
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
+ editQueryTimeoutSecs.value = settingsStore.editorSettings.queryTimeoutSecs;
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation;
editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode;
@@ -238,6 +240,7 @@ function hasChanges(): boolean {
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
editShowTrayIcon.value !== settingsStore.desktopSettings.show_tray_icon ||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
+ editQueryTimeoutSecs.value !== settingsStore.editorSettings.queryTimeoutSecs ||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation ||
editAutoSelectActiveSidebarNode.value !== settingsStore.editorSettings.autoSelectActiveSidebarNode ||
@@ -257,6 +260,7 @@ async function applySettings() {
wordWrap: editWordWrap.value,
appLayout: editAppLayout.value,
redisScanPageSize: editRedisScanPageSize.value,
+ queryTimeoutSecs: editQueryTimeoutSecs.value,
shortcuts: editShortcuts.value,
sidebarActivation: editSidebarActivation.value,
autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value,
@@ -278,6 +282,7 @@ function resetDefaults() {
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
+ editQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.queryTimeoutSecs;
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation;
editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode;
@@ -884,6 +889,26 @@ watch(
+
+
+
{
+ const n = Number(v);
+ if (Number.isFinite(n) && n >= 0) editQueryTimeoutSecs = n;
+ }
+ "
+ class="h-9 w-28"
+ />
+
{{ t("settings.queryTimeoutSecsDescription") }}
+
+
+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 05334ff8d..403e9de84 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1367,6 +1367,8 @@ export default {
shortcutPressShortcut: "Press shortcut",
shortcutConflict: "This shortcut conflicts with another action in the same scope.",
preview: "Live Preview",
+ queryTimeoutSecs: "Query Timeout (seconds)",
+ queryTimeoutSecsDescription: "Maximum execution time per query. 0 = no timeout. Default 30s.",
jdbcPlugin: "DBX JDBC plugin",
jdbcPluginInstall: "Install JDBC plugin",
jdbcPluginInstallSuccess: "JDBC plugin installed",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index ff6a4c18e..72da02f0b 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1342,6 +1342,8 @@ export default {
shortcutPressShortcut: "按下快捷键",
shortcutConflict: "这个快捷键与同一作用域内的其他操作冲突。",
preview: "实时预览",
+ queryTimeoutSecs: "查询超时(秒)",
+ queryTimeoutSecsDescription: "每条查询的最大执行时间。0 = 不限制。默认 30 秒。",
jdbcPlugin: "DBX JDBC 插件",
jdbcPluginInstall: "安装 JDBC 插件",
jdbcPluginInstallSuccess: "JDBC 插件已安装",
diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts
index 9d786ea91..a05b6c027 100644
--- a/apps/desktop/src/lib/http.ts
+++ b/apps/desktop/src/lib/http.ts
@@ -413,6 +413,7 @@ export async function executeQuery(
pageSize?: number;
resultSessionId?: string;
clientSessionId?: string;
+ timeoutSecs?: number;
},
): Promise {
return post("/api/query/execute", { connectionId, database, sql, schema, executionId, ...options });
@@ -430,6 +431,7 @@ export async function executeMulti(
pageSize?: number;
resultSessionId?: string;
clientSessionId?: string;
+ timeoutSecs?: number;
},
): Promise {
return post("/api/query/execute-multi", { connectionId, database, sql, schema, executionId, ...options });
diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts
index 20814e370..457efaa10 100644
--- a/apps/desktop/src/lib/tauri.ts
+++ b/apps/desktop/src/lib/tauri.ts
@@ -367,6 +367,7 @@ export async function executeQuery(
pageSize?: number;
resultSessionId?: string;
clientSessionId?: string;
+ timeoutSecs?: number;
},
): Promise {
return invoke("execute_query", { connectionId, database, sql, schema, executionId, ...options });
@@ -384,6 +385,7 @@ export async function executeMulti(
pageSize?: number;
resultSessionId?: string;
clientSessionId?: string;
+ timeoutSecs?: number;
},
): Promise {
return invoke("execute_multi", { connectionId, database, sql, schema, executionId, ...options });
diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts
index 6e0625bb1..60fae3e3f 100644
--- a/apps/desktop/src/stores/queryStore.ts
+++ b/apps/desktop/src/stores/queryStore.ts
@@ -643,18 +643,20 @@ export const useQueryStore = defineStore("query", () => {
}
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
- const executionOptions =
- typeof pageLimit === "number"
+ const executionOptions = {
+ ...(typeof pageLimit === "number"
? useAgentResultSession
? {
maxRows: pageLimit,
fetchSize: pageLimit,
pageSize: pageLimit,
resultSessionId: options?.pagination?.sessionId,
- clientSessionId: tab.id,
}
- : { maxRows: pageLimit, fetchSize: pageLimit, clientSessionId: tab.id }
- : undefined;
+ : { maxRows: pageLimit, fetchSize: pageLimit }
+ : {}),
+ clientSessionId: tab.id,
+ timeoutSecs: settingsStore.editorSettings.queryTimeoutSecs,
+ };
const results = await api.executeMulti(
tab.connectionId,
tab.database,
@@ -755,6 +757,7 @@ export const useQueryStore = defineStore("query", () => {
try {
const result = await api.executeQuery(tab.connectionId, tab.database, built.sql, tab.schema, executionId, {
clientSessionId: tab.id,
+ timeoutSecs: useSettingsStore().editorSettings.queryTimeoutSecs,
});
const current = tabs.value.find((t) => t.id === id);
if (current?.explainExecutionId === executionId) {
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 45cc4aa43..ea6116cb3 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -187,6 +187,8 @@ export interface EditorSettings {
columnFormatters: Record;
customColumnFormatters: Record;
snippets: SqlSnippet[];
+ /** Query timeout in seconds. 0 = no timeout. Default 30s. */
+ queryTimeoutSecs: number;
}
export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [
@@ -232,6 +234,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
columnFormatters: {},
customColumnFormatters: {},
snippets: DEFAULT_SQL_SNIPPETS,
+ queryTimeoutSecs: 30,
};
export const STORAGE_KEY = "dbx-editor-settings";
@@ -283,6 +286,11 @@ function normalizeSqlSnippets(value: unknown, existing?: SqlSnippet[]): SqlSnipp
return valid;
}
+function normalizeQueryTimeoutSecs(value: unknown): number {
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
+ return DEFAULT_EDITOR_SETTINGS.queryTimeoutSecs;
+}
+
export function normalizeEditorSettings(settings: Partial, existing?: EditorSettings): EditorSettings {
return {
fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily,
@@ -306,6 +314,7 @@ export function normalizeEditorSettings(settings: Partial, exist
columnFormatters: normalizeColumnFormatters(settings.columnFormatters),
customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters),
snippets: normalizeSqlSnippets(settings.snippets, existing?.snippets),
+ queryTimeoutSecs: normalizeQueryTimeoutSecs(settings.queryTimeoutSecs),
};
}
@@ -408,6 +417,9 @@ export const useSettingsStore = defineStore("settings", () => {
const normalizedPartial = {
...partial,
...(partial.pageSize !== undefined ? { pageSize: normalizeResultPageSize(partial.pageSize) } : {}),
+ ...(partial.queryTimeoutSecs !== undefined
+ ? { queryTimeoutSecs: normalizeQueryTimeoutSecs(partial.queryTimeoutSecs) }
+ : {}),
...(partial.sidebarHiddenTablePrefixes !== undefined
? { sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes) }
: {}),
diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs
index d4d2a3f0c..9b909632a 100644
--- a/crates/dbx-core/src/query.rs
+++ b/crates/dbx-core/src/query.rs
@@ -27,6 +27,9 @@ pub struct QueryExecutionOptions {
pub page_size: Option,
pub result_session_id: Option,
pub client_session_id: Option,
+ /// Query timeout in seconds. `None` uses the default (30s).
+ /// `Some(0)` disables the timeout entirely.
+ pub timeout_secs: Option,
}
fn query_result_row_limit(max_rows: Option) -> usize {
@@ -399,6 +402,39 @@ where
}
}
+/// Like `wait_for_query_with_timeout` but with an optional timeout.
+/// `None` means no timeout (only cancellation can stop the query).
+pub async fn wait_for_query_opt(
+ cancel_token: Option,
+ timeout_duration: Option,
+ future: F,
+) -> Result
+where
+ F: Future