feat(timeout): support global connection and query timeout settings
This commit is contained in:
26 files changed
+588
-29
No files matched your search
+11
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user