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

This commit is contained in:
zipg authored and GitHub committed 2026-08-09 13:57:42 +08:00
1 parent 5c343ab416
commit 72ff141f02
26 files changed
+588 -29

No files matched your search

+11 -1
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);
});
});
@@ -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" },
+6 -2
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;
}