feat(timeout): support global connection and query timeout settings
This commit is contained in:
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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user