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

@@ -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"));
+14
View File
@@ -220,6 +220,20 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ cellDetailMetadataCollapsed: true }).cellDetailMetadataCollapsed).toBe(true);
});
it("normalizes the global query timeout and inherited connection ids", () => {
expect(normalizeEditorSettings({}).globalConnectTimeoutSecs).toBe(10);
expect(normalizeEditorSettings({ globalConnectTimeoutSecs: 0 }).globalConnectTimeoutSecs).toBe(1);
expect(normalizeEditorSettings({}).globalQueryTimeoutSecs).toBe(30);
expect(normalizeEditorSettings({ queryTimeoutSecs: 45 } as any).globalQueryTimeoutSecs).toBe(45);
expect(normalizeEditorSettings({ globalQueryTimeoutSecs: -1 }).globalQueryTimeoutSecs).toBe(0);
expect(normalizeEditorSettings({ globalQueryTimeoutSecs: 301 }).globalQueryTimeoutSecs).toBe(300);
expect(normalizeEditorSettings({ connectTimeoutInheritConnectionIds: ["one", "one", " ", "two"] }).connectTimeoutInheritConnectionIds).toEqual(["one", "two"]);
expect(normalizeEditorSettings({ queryTimeoutInheritConnectionIds: ["one", "one", " ", "two"] }).queryTimeoutInheritConnectionIds).toEqual(["one", "two"]);
expect(normalizeEditorSettings({}).timeoutInheritanceMigrationVersion).toBe(0);
expect(normalizeEditorSettings({ queryTimeoutInheritanceMigrationVersion: 1 } as any).timeoutInheritanceMigrationVersion).toBe(1);
expect(normalizeEditorSettings({ timeoutInheritanceMigrationVersion: 2 }).timeoutInheritanceMigrationVersion).toBe(2);
});
it("normalizes toolbar item settings from older saved settings", () => {
const settings = normalizeEditorSettings({
toolbarItems: {
+119 -3
View File
@@ -71,6 +71,7 @@ import { findDatabaseTreeNode } from "@/lib/sidebar/treeRefreshTarget";
import { simpleModeEmptyShellNeedsConfirmedLoad, treeNodeLoadedChildrenContentPresent } from "@/lib/sidebar/treeLoadedChildrenMarker";
import { shouldMarkDisconnected } from "@/lib/connection/connectionHealth";
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
import { loadTimeoutInheritanceBackup, saveTimeoutInheritanceBackup } from "@/lib/connection/timeoutInheritanceBackup";
import { migrateSqlServerLegacyCompatibilityConfig, requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { deleteTabResultSnapshotsForOwner } from "@/lib/tabs/tabResultCache";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
@@ -975,6 +976,8 @@ export const useConnectionStore = defineStore("connection", () => {
function normalizeConnection(config: ConnectionConfig): ConnectionConfig {
config = { ...config };
migrateSqlServerLegacyCompatibilityConfig(config);
const connectTimeoutInherit = config.connect_timeout_inherit ?? settingsStore.editorSettings.connectTimeoutInheritConnectionIds.includes(config.id);
const queryTimeoutInherit = config.query_timeout_inherit ?? settingsStore.editorSettings.queryTimeoutInheritConnectionIds.includes(config.id);
const labelMap: Record<string, string> = {
mysql: "MySQL",
postgres: "PostgreSQL",
@@ -1065,8 +1068,10 @@ export const useConnectionStore = defineStore("connection", () => {
docs_notes_path: config.docs_notes_path?.trim() ? config.docs_notes_path.trim() : undefined,
transport_layers: Array.isArray(config.transport_layers) ? config.transport_layers : [],
show_system_schemas: config.show_system_schemas === true,
connect_timeout_secs: config.connect_timeout_secs || 10,
query_timeout_secs: config.query_timeout_secs ?? 30,
connect_timeout_secs: connectTimeoutInherit ? settingsStore.editorSettings.globalConnectTimeoutSecs : config.connect_timeout_secs || 10,
connect_timeout_inherit: connectTimeoutInherit,
query_timeout_secs: queryTimeoutInherit ? settingsStore.editorSettings.globalQueryTimeoutSecs : (config.query_timeout_secs ?? 30),
query_timeout_inherit: queryTimeoutInherit,
idle_timeout_secs: config.idle_timeout_secs ?? 60,
keepalive_interval_secs: config.keepalive_interval_secs ?? DEFAULT_KEEPALIVE_INTERVAL_SECS,
redis_database_aliases: normalizeRedisDatabaseAliases(config.redis_database_aliases),
@@ -2274,6 +2279,7 @@ export const useConnectionStore = defineStore("connection", () => {
async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) {
const normalized = normalizeConnection(config);
await persistTimeoutInheritance(normalized.id, normalized.connect_timeout_inherit === true, normalized.query_timeout_inherit === true);
const existing = connections.value.findIndex((c) => c.id === normalized.id);
const nextConnections = [...connections.value];
if (existing >= 0) {
@@ -2285,6 +2291,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
await persistConnections(nextConnections);
connections.value = nextConnections;
syncTimeoutInheritanceBackup();
rebuildTreeNodes();
persistSidebarLayoutDebounced();
stopCreatingConnectionInGroup();
@@ -2386,7 +2393,12 @@ export const useConnectionStore = defineStore("connection", () => {
const removedIds = new Set(connectionIds);
const nextConnections = connections.value.filter((c) => !removedIds.has(c.id));
await persistConnections(nextConnections);
await persistTimeoutInheritanceIds(
settingsStore.editorSettings.connectTimeoutInheritConnectionIds.filter((id) => !removedIds.has(id)),
settingsStore.editorSettings.queryTimeoutInheritConnectionIds.filter((id) => !removedIds.has(id)),
);
connections.value = nextConnections;
syncTimeoutInheritanceBackup();
let nextPinnedOrder = pinnedTreeNodeOrder.value;
for (const id of removedIds) {
const prefix = `${id}:`;
@@ -2431,8 +2443,10 @@ export const useConnectionStore = defineStore("connection", () => {
const runtimeConfigChanged = connectionConfigFingerprint(connections.value[idx]) !== connectionConfigFingerprint(config);
const nextConnections = [...connections.value];
nextConnections[idx] = config;
await persistTimeoutInheritance(config.id, config.connect_timeout_inherit === true, config.query_timeout_inherit === true);
await persistConnections(nextConnections);
connections.value = nextConnections;
syncTimeoutInheritanceBackup();
rebuildTreeNodes();
if (!runtimeConfigChanged) return;
clearPrimaryVisibleObjectNames(config.id);
@@ -6378,6 +6392,95 @@ export const useConnectionStore = defineStore("connection", () => {
await api.saveConnections(nextConnections.filter((connection) => connection.one_time !== true));
}
function sameIds(left: string[], right: string[]) {
return left.length === right.length && left.every((id, index) => id === right[index]);
}
async function persistTimeoutInheritanceIds(connectIds: string[], queryIds: string[]) {
if (sameIds(connectIds, settingsStore.editorSettings.connectTimeoutInheritConnectionIds) && sameIds(queryIds, settingsStore.editorSettings.queryTimeoutInheritConnectionIds)) return;
settingsStore.updateEditorSettings({
connectTimeoutInheritConnectionIds: connectIds,
queryTimeoutInheritConnectionIds: queryIds,
});
await settingsStore.persistEditorSettings();
}
async function persistTimeoutInheritance(connectionId: string, connectInherit: boolean, queryInherit: boolean) {
const connectIds = new Set(settingsStore.editorSettings.connectTimeoutInheritConnectionIds);
const queryIds = new Set(settingsStore.editorSettings.queryTimeoutInheritConnectionIds);
if (connectInherit) connectIds.add(connectionId);
else connectIds.delete(connectionId);
if (queryInherit) queryIds.add(connectionId);
else queryIds.delete(connectionId);
await persistTimeoutInheritanceIds([...connectIds], [...queryIds]);
}
function syncTimeoutInheritanceBackup(source: ConnectionConfig[] = connections.value) {
const connectSnapshots: Record<string, number> = {};
const querySnapshots: Record<string, number> = {};
for (const connection of source) {
if (connection.connect_timeout_inherit === true) connectSnapshots[connection.id] = connection.connect_timeout_secs || settingsStore.editorSettings.globalConnectTimeoutSecs;
if (connection.query_timeout_inherit === true) querySnapshots[connection.id] = connection.query_timeout_secs ?? settingsStore.editorSettings.globalQueryTimeoutSecs;
}
saveTimeoutInheritanceBackup({
version: 1,
globalConnectTimeoutSecs: settingsStore.editorSettings.globalConnectTimeoutSecs,
globalQueryTimeoutSecs: settingsStore.editorSettings.globalQueryTimeoutSecs,
connectSnapshots,
querySnapshots,
});
}
async function applyGlobalTimeouts({ connectTimeoutSecs, queryTimeoutSecs }: { connectTimeoutSecs?: number; queryTimeoutSecs?: number }) {
const nextConnections = connections.value.map((connection) => {
const nextConnectTimeout = connectTimeoutSecs !== undefined && connection.connect_timeout_inherit === true ? connectTimeoutSecs : connection.connect_timeout_secs;
const nextQueryTimeout = queryTimeoutSecs !== undefined && connection.query_timeout_inherit === true ? queryTimeoutSecs : connection.query_timeout_secs;
if (nextConnectTimeout === connection.connect_timeout_secs && nextQueryTimeout === connection.query_timeout_secs) return connection;
return { ...connection, connect_timeout_secs: nextConnectTimeout, query_timeout_secs: nextQueryTimeout };
});
if (nextConnections.some((connection, index) => connection !== connections.value[index])) {
await persistConnections(nextConnections);
connections.value = nextConnections;
}
syncTimeoutInheritanceBackup();
}
async function migrateTimeoutInheritance(saved: ConnectionConfig[]) {
const migrationVersion = settingsStore.editorSettings.timeoutInheritanceMigrationVersion;
const backup = loadTimeoutInheritanceBackup();
const connectIdsBefore = new Set(settingsStore.editorSettings.connectTimeoutInheritConnectionIds);
const queryIdsBefore = new Set(settingsStore.editorSettings.queryTimeoutInheritConnectionIds);
const globalConnectTimeoutSecs = migrationVersion < 2 && backup ? backup.globalConnectTimeoutSecs : settingsStore.editorSettings.globalConnectTimeoutSecs;
const globalQueryTimeoutSecs = migrationVersion < 2 && backup ? backup.globalQueryTimeoutSecs : settingsStore.editorSettings.globalQueryTimeoutSecs;
const resolveInheritance = (connection: ConnectionConfig, scope: "connect" | "query") => {
const explicit = scope === "connect" ? connection.connect_timeout_inherit : connection.query_timeout_inherit;
if (explicit === true || explicit === false) return explicit;
const ids = scope === "connect" ? connectIdsBefore : queryIdsBefore;
const snapshots = scope === "connect" ? backup?.connectSnapshots : backup?.querySnapshots;
const value = Number(scope === "connect" ? (connection.connect_timeout_secs ?? 10) : (connection.query_timeout_secs ?? 30));
const snapshot = snapshots?.[connection.id];
if (snapshot !== undefined && (ids.has(connection.id) || migrationVersion < 2)) return value === snapshot;
if (ids.has(connection.id)) return true;
if (scope === "connect" && migrationVersion < 2) return value === 10;
if (scope === "query" && migrationVersion < 1) return value === 30;
return false;
};
const connectIds = saved.filter((connection) => resolveInheritance(connection, "connect")).map((connection) => connection.id);
const queryIds = saved.filter((connection) => resolveInheritance(connection, "query")).map((connection) => connection.id);
settingsStore.updateEditorSettings({
globalConnectTimeoutSecs,
connectTimeoutInheritConnectionIds: connectIds,
globalQueryTimeoutSecs,
queryTimeoutInheritConnectionIds: queryIds,
timeoutInheritanceMigrationVersion: 2,
});
if (migrationVersion !== 2 || !sameIds(connectIds, [...connectIdsBefore]) || !sameIds(queryIds, [...queryIdsBefore])) {
await settingsStore.persistEditorSettings();
}
}
function persistSidebarLayoutDebounced() {
if (layoutPersistTimer) clearTimeout(layoutPersistTimer);
layoutPersistTimer = setTimeout(() => {
@@ -6506,7 +6609,14 @@ export const useConnectionStore = defineStore("connection", () => {
const { encryptConfig } = await import("@/lib/backend/configCrypto");
const tunnelProfileStore = useTunnelProfileStore();
await tunnelProfileStore.init();
const exportData = { connections: connections.value, layout: sidebarLayout.value, tunnelProfiles: tunnelProfileStore.profiles };
// Older DBX versions ignore inheritance flags, so always include the
// effective numeric values as a backward-compatible snapshot.
const exportedConnections = connections.value.map((connection) => ({
...connection,
connect_timeout_secs: connection.connect_timeout_inherit === true ? settingsStore.editorSettings.globalConnectTimeoutSecs : connection.connect_timeout_secs,
query_timeout_secs: connection.query_timeout_inherit === true ? settingsStore.editorSettings.globalQueryTimeoutSecs : connection.query_timeout_secs,
}));
const exportData = { connections: exportedConnections, layout: sidebarLayout.value, tunnelProfiles: tunnelProfileStore.profiles };
const json = JSON.stringify(exportData);
const payload = await encryptConfig(json, passphrase);
const content = JSON.stringify(payload, null, 2);
@@ -6886,7 +6996,12 @@ export const useConnectionStore = defineStore("connection", () => {
initFromDiskPromise = (async () => {
const [pinnedOrder, saved] = await Promise.all([loadPinnedTreeNodeOrder(), api.loadConnections(), tunnelProfileStore.init()]);
setPinnedTreeNodeOrder(pinnedOrder);
await migrateTimeoutInheritance(saved);
connections.value = saved.map(normalizeConnection);
if (connections.value.some((connection, index) => (connection.connect_timeout_inherit === true && connection.connect_timeout_secs !== saved[index]?.connect_timeout_secs) || (connection.query_timeout_inherit === true && connection.query_timeout_secs !== saved[index]?.query_timeout_secs))) {
await persistConnections();
}
syncTimeoutInheritanceBackup();
const savedLayout = await api.loadSidebarLayout();
const currentLayout = sidebarLayout.value.groups.length || sidebarLayout.value.order.length ? sidebarLayout.value : null;
sidebarLayout.value = reconcileLayout(
@@ -6964,6 +7079,7 @@ export const useConnectionStore = defineStore("connection", () => {
pasteConnectionClipboard,
addEphemeralConnection,
updateConnection,
applyGlobalTimeouts,
updateConnectionDatabaseInfo,
setDefaultDatabase,
clearDefaultDatabase,
+6 -7
View File
@@ -3553,8 +3553,7 @@ export const useQueryStore = defineStore("query", () => {
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog);
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const settingsStore = useSettingsStore();
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs);
const statementExecution = tab.mode === "query" ? createBatchSqlExecution(executionId, tab.sql, sql, effectiveDbType, options?.sourceOffset) : undefined;
tab.batchSqlExecution = statementExecution && (tab.autoCommit !== false || statementExecution.total === 1) ? statementExecution : undefined;
if (tab.batchSqlExecution) liveBatchSqlExecutions.set(tab, tab.batchSqlExecution);
@@ -4411,7 +4410,7 @@ export const useQueryStore = defineStore("query", () => {
const tab = tabs.value.find((t) => t.id === id);
if (!tab) return { ok: false as const, reason: "empty" as const };
const conn = useConnectionStore().getConfig(tab.connectionId);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs);
const executionId = uuid();
tab.isExplaining = true;
@@ -5091,7 +5090,7 @@ export const useQueryStore = defineStore("query", () => {
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys;
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
const orderBy = tab.orderByInput?.trim() || sortOrder;
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs);
const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tableMeta.catalog);
const rows: QueryResult["rows"] = [];
let columns: string[] = [];
@@ -5157,7 +5156,7 @@ export const useQueryStore = defineStore("query", () => {
await connStore.ensureConnected(tab.connectionId);
const conn = connStore.getConfig(tab.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs);
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type);
const queryBaseSql = queryResultBaseSql(tab);
const exportSettings = useSettingsStore().editorSettings;
@@ -5324,7 +5323,7 @@ export const useQueryStore = defineStore("query", () => {
pageSize: settings.exportBatchSize,
rowLimit,
totalRows,
timeoutSecs: queryTimeoutSecsForConnection(conn),
timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs),
keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled,
clientSessionId,
executionId: uuid(),
@@ -5360,7 +5359,7 @@ export const useQueryStore = defineStore("query", () => {
pageSize: settings.exportBatchSize,
rowLimit: settings.exportRowLimitEnabled ? settings.exportRowLimit : null,
totalRows: null,
timeoutSecs: queryTimeoutSecsForConnection(conn),
timeoutSecs: queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs),
keysetOptimizationEnabled: settings.queryExportKeysetOptimizationEnabled,
clientSessionId: `${tabClientSessionId(tab, "export")}:${exportId}`,
executionId: uuid(),
+40
View File
@@ -466,6 +466,11 @@ export interface EditorSettings {
activeCustomThemeId: string;
executeMode: "all" | "current";
executeModeDefaultVersion: number;
globalConnectTimeoutSecs: number;
connectTimeoutInheritConnectionIds: string[];
globalQueryTimeoutSecs: number;
queryTimeoutInheritConnectionIds: string[];
timeoutInheritanceMigrationVersion: number;
showExecutionTargetPicker: boolean;
showStatementRunButtons: boolean;
showCurrentStatementFrame: boolean;
@@ -647,6 +652,11 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
activeCustomThemeId: "default",
executeMode: "current",
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
globalConnectTimeoutSecs: 10,
connectTimeoutInheritConnectionIds: [],
globalQueryTimeoutSecs: 30,
queryTimeoutInheritConnectionIds: [],
timeoutInheritanceMigrationVersion: 2,
showExecutionTargetPicker: false,
showStatementRunButtons: true,
showCurrentStatementFrame: true,
@@ -742,6 +752,16 @@ const LEGACY_DEFAULT_EXPORT_BATCH_SIZE = 10000;
const MIN_UI_SCALE = 0.75;
const MAX_UI_SCALE = 2;
export function normalizeGlobalQueryTimeoutSecs(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs;
return Math.min(300, Math.max(0, Math.round(value)));
}
export function normalizeGlobalConnectTimeoutSecs(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs;
return Math.min(300, Math.max(1, Math.round(value)));
}
function normalizeUiScale(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.uiScale;
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value * 100) / 100));
@@ -914,6 +934,7 @@ function normalizeTableInfoTab(value: unknown): TableInfoTab {
}
export function normalizeEditorSettings(settings: Partial<EditorSettings>, existing?: EditorSettings): EditorSettings {
const legacyTimeoutSettings = settings as Partial<EditorSettings> & { queryTimeoutSecs?: unknown; queryTimeoutInheritanceMigrationVersion?: unknown };
const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled);
const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion;
const executeModeDefaultVersion = typeof savedExecuteModeDefaultVersion === "number" && savedExecuteModeDefaultVersion >= EXECUTE_MODE_CURRENT_DEFAULT_VERSION ? savedExecuteModeDefaultVersion : EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
@@ -959,6 +980,16 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
activeCustomThemeId: settings.activeCustomThemeId ?? "default",
executeMode: hasCurrentExecuteModeDefault && (settings.executeMode === "all" || settings.executeMode === "current") ? settings.executeMode : DEFAULT_EDITOR_SETTINGS.executeMode,
executeModeDefaultVersion,
globalConnectTimeoutSecs: normalizeGlobalConnectTimeoutSecs(settings.globalConnectTimeoutSecs),
connectTimeoutInheritConnectionIds: Array.isArray(settings.connectTimeoutInheritConnectionIds) ? [...new Set(settings.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [],
globalQueryTimeoutSecs: normalizeGlobalQueryTimeoutSecs(settings.globalQueryTimeoutSecs ?? legacyTimeoutSettings.queryTimeoutSecs),
queryTimeoutInheritConnectionIds: Array.isArray(settings.queryTimeoutInheritConnectionIds) ? [...new Set(settings.queryTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [],
timeoutInheritanceMigrationVersion:
typeof settings.timeoutInheritanceMigrationVersion === "number" && settings.timeoutInheritanceMigrationVersion >= 1
? Math.floor(settings.timeoutInheritanceMigrationVersion)
: typeof legacyTimeoutSettings.queryTimeoutInheritanceMigrationVersion === "number" && legacyTimeoutSettings.queryTimeoutInheritanceMigrationVersion >= 1
? 1
: 0,
showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker,
showStatementRunButtons: typeof settings.showStatementRunButtons === "boolean" ? settings.showStatementRunButtons : DEFAULT_EDITOR_SETTINGS.showStatementRunButtons,
showCurrentStatementFrame: typeof settings.showCurrentStatementFrame === "boolean" ? settings.showCurrentStatementFrame : DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame,
@@ -1449,6 +1480,15 @@ export const useSettingsStore = defineStore("settings", () => {
}
}
if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode;
if (partial.globalConnectTimeoutSecs !== undefined) editorSettings.value.globalConnectTimeoutSecs = normalizeGlobalConnectTimeoutSecs(partial.globalConnectTimeoutSecs);
if (partial.connectTimeoutInheritConnectionIds !== undefined) {
editorSettings.value.connectTimeoutInheritConnectionIds = [...new Set(partial.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];
}
if (partial.globalQueryTimeoutSecs !== undefined) editorSettings.value.globalQueryTimeoutSecs = normalizeGlobalQueryTimeoutSecs(partial.globalQueryTimeoutSecs);
if (partial.queryTimeoutInheritConnectionIds !== undefined) {
editorSettings.value.queryTimeoutInheritConnectionIds = [...new Set(partial.queryTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];
}
if (partial.timeoutInheritanceMigrationVersion !== undefined) editorSettings.value.timeoutInheritanceMigrationVersion = Math.max(0, Math.floor(partial.timeoutInheritanceMigrationVersion));
if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker;
if (partial.showStatementRunButtons !== undefined) editorSettings.value.showStatementRunButtons = partial.showStatementRunButtons === true;
if (partial.showCurrentStatementFrame !== undefined) editorSettings.value.showCurrentStatementFrame = partial.showCurrentStatementFrame === true;