fix(settings): keep persisted query timeout across upgrades

This commit is contained in:
弈秋 authored and GitHub committed 2026-09-23 14:55:47 +08:00
1 parent ebb2c1006e
commit de5fde242f
3 files changed
+109 -2

No files matched your search

@@ -312,6 +312,88 @@ describe("connectionStore timeout recovery", () => {
);
});
it("keeps a persisted global query timeout over a stale backup when upgrading", async () => {
// Reproduces #10024: the user saved a 60s global query timeout, but the
// timeout-inheritance migration re-sourced the global value from a stale
// localStorage backup (30s) on the first launch of the new build, resetting
// the saved setting. The persisted blob already carries the value, so it must
// win over the backup.
localStorage.setItem(
"dbx-timeout-inheritance-backup-v1",
JSON.stringify({
version: 1,
globalConnectTimeoutSecs: 10,
globalQueryTimeoutSecs: 30,
connectSnapshots: {},
querySnapshots: {},
}),
);
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadEditorSettings: vi.fn().mockResolvedValue({
globalConnectTimeoutSecs: 20,
globalQueryTimeoutSecs: 60,
timeoutInheritanceMigrationVersion: 1,
}),
loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "custom", connect_timeout_secs: 45, query_timeout_secs: 300 })]),
loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]),
loadSidebarLayout: vi.fn().mockResolvedValue(null),
loadTableVGroups: vi.fn().mockResolvedValue({}),
loadTunnelProfiles: vi.fn().mockResolvedValue([]),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveEditorSettings,
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useSettingsStore } = await import("@/stores/settingsStore");
const settingsStore = useSettingsStore();
const { useConnectionStore } = await import("@/stores/connectionStore");
await useConnectionStore().initFromDisk();
expect(settingsStore.editorSettings.globalQueryTimeoutSecs).toBe(60);
expect(settingsStore.editorSettings.globalConnectTimeoutSecs).toBe(20);
expect(saveEditorSettings).toHaveBeenCalledWith(expect.objectContaining({ globalQueryTimeoutSecs: 60, globalConnectTimeoutSecs: 20 }));
});
it("still recovers an absent global query timeout from the backup after a downgrade", async () => {
// The migration must keep recovering the global value from the backup when
// the persisted settings predate the setting entirely (no global keys on
// disk) — the case the backup precedence was originally written for.
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),
loadEditorSettings: vi.fn().mockResolvedValue({ timeoutInheritanceMigrationVersion: 1 }),
loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "inherited", connect_timeout_secs: 7, query_timeout_secs: 12 })]),
loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]),
loadSidebarLayout: vi.fn().mockResolvedValue(null),
loadTableVGroups: vi.fn().mockResolvedValue({}),
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();
const { useConnectionStore } = await import("@/stores/connectionStore");
await useConnectionStore().initFromDisk();
expect(settingsStore.editorSettings.globalQueryTimeoutSecs).toBe(12);
expect(settingsStore.editorSettings.queryTimeoutInheritConnectionIds).toContain("inherited");
});
it("does not reclassify local default-valued overrides after migration", async () => {
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
+9 -2
View File
@@ -9002,8 +9002,15 @@ export const useConnectionStore = defineStore("connection", () => {
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;
// Recover the global timeout from the localStorage backup only when the
// settings blob on disk never carried one — the downgrade case, where an
// older build predated the setting. When the user's persisted value exists it
// is authoritative and must win over a backup that can lag behind it (the
// upgrade case, where a stale backup otherwise reset a saved timeout).
const recoverGlobalConnectFromBackup = migrationVersion < 2 && !!backup && !settingsStore.hasPersistedGlobalTimeout("connect");
const recoverGlobalQueryFromBackup = migrationVersion < 2 && !!backup && !settingsStore.hasPersistedGlobalTimeout("query");
const globalConnectTimeoutSecs = recoverGlobalConnectFromBackup ? backup!.globalConnectTimeoutSecs : settingsStore.editorSettings.globalConnectTimeoutSecs;
const globalQueryTimeoutSecs = recoverGlobalQueryFromBackup ? backup!.globalQueryTimeoutSecs : settingsStore.editorSettings.globalQueryTimeoutSecs;
const resolveInheritance = (connection: ConnectionConfig, scope: "connect" | "query") => {
const explicit = scope === "connect" ? connection.connect_timeout_inherit : connection.query_timeout_inherit;
+18
View File
@@ -1918,6 +1918,19 @@ export const useSettingsStore = defineStore("settings", () => {
persistedAlwaysOnTopToolbarVisibility = visible;
}
// Whether the editor-settings blob loaded from disk actually carried a global
// timeout value. A normalized number is not enough to tell a user's saved
// choice apart from the built-in default filled in by normalizeEditorSettings,
// and the timeout-inheritance migration relies on that distinction: a persisted
// value must win over the localStorage backup, while a value that was never on
// disk (a downgrade, where the older build predated the setting) is recovered
// from the backup. Tracked here at load time and read by the migration.
const persistedGlobalTimeoutScopes = ref({ connect: false, query: false });
function hasPersistedGlobalTimeout(scope: "connect" | "query"): boolean {
return persistedGlobalTimeoutScopes.value[scope];
}
function enqueueEditorSettingsOperation<T>(operation: () => Promise<T>): Promise<T> {
const queuedOperation = editorSettingsOperationQueue ? editorSettingsOperationQueue.then(operation) : operation();
const trackedOperation = queuedOperation.then(
@@ -1996,6 +2009,10 @@ export const useSettingsStore = defineStore("settings", () => {
}
editorSettings.value = normalized;
persistedAlwaysOnTopToolbarVisibility = normalized.toolbarItems.alwaysOnTop;
persistedGlobalTimeoutScopes.value = {
connect: typeof savedSettings.globalConnectTimeoutSecs === "number",
query: typeof savedSettings.globalQueryTimeoutSecs === "number" || typeof (savedSettings as { queryTimeoutSecs?: unknown }).queryTimeoutSecs === "number",
};
const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
const needsTabNavigationShortcutMigration = needsTabNavigationHistoryShortcutMigration(savedSettings.shortcuts);
const savedNullText = (savedSettings.dataGridExtractorOptions as Partial<DataGridExtractorOptions> | undefined)?.dsv?.nullText;
@@ -2805,6 +2822,7 @@ export const useSettingsStore = defineStore("settings", () => {
isConfigured,
isEditorSettingsLoaded,
editorSettings,
hasPersistedGlobalTimeout,
desktopSettings,
mcpGlobalPolicy,
initEditorSettings,