Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Restored table tabs no longer reload all at once or flood failure dialogs on launch. Only the frontmost tab loads immediately; other restored tabs load when you switch to them, and a load failure now shows inline in the tab instead of a dialog. (#1796)

## [0.54.0] - 2026-06-30

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,8 @@ extension QueryExecutionCoordinator {
_ error: Error,
sql: String,
tabId: UUID,
connection conn: DatabaseConnection
connection conn: DatabaseConnection,
trigger: TableLoadTrigger = .userInitiated
) {
parent.currentQueryTask = nil
parent.tabManager.mutate(tabId: tabId) { tab in
Expand All @@ -505,6 +506,8 @@ extension QueryExecutionCoordinator {
errorMessage: error.localizedDescription
)

guard !trigger.suppressesFailureModal else { return }

let errorMessage = error.localizedDescription
let queryCopy = sql
Task { [weak self, parent] in
Expand All @@ -529,14 +532,14 @@ extension QueryExecutionCoordinator {
}
}

func restoreSchemaAndRunQuery(_ schema: String) async {
func restoreSchemaAndRunQuery(_ schema: String, trigger: TableLoadTrigger = .userInitiated) async {
guard let driver = DatabaseManager.shared.driver(for: parent.connectionId) else {
parent.needsLazyLoad = true
parent.pendingLoadTrigger = trigger
return
}
guard let schemaDriver = driver as? SchemaSwitchable,
schemaDriver.currentSchema != nil else {
parent.runQuery()
parent.runQuery(trigger: trigger)
return
}
do {
Expand All @@ -550,7 +553,7 @@ extension QueryExecutionCoordinator {
helpersLogger.warning("Failed to restore schema '\(schema, privacy: .public)': \(error.localizedDescription, privacy: .public)")
return
}
parent.runQuery()
parent.runQuery(trigger: trigger)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ internal final class AppLaunchCoordinator {
window.close()
}
case .reopenLast:
reopenLastSessionIfArchiveMissing()
reopenLastSession()
}
}

private func reopenLastSessionIfArchiveMissing() {
private func reopenLastSession() {
guard !NSApp.windows.contains(where: { Self.isMainWindow($0) }) else { return }

let connectionIds = LastOpenConnectionsStorage.shared.load()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@

import Foundation

enum RestoreLoadTiming {
case immediate
case deferred
}

@MainActor
enum RestorationGroupRegistry {
struct WindowGroup {
let tabs: [QueryTab]
let selectedTabId: UUID?
var loadTiming: RestoreLoadTiming = .immediate
}

private static var groups: [UUID: WindowGroup] = [:]
Expand Down
15 changes: 15 additions & 0 deletions TablePro/Core/Services/Infrastructure/RestoreWindowPlan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// RestoreWindowPlan.swift
// TablePro
//

import Foundation

enum RestoreWindowPlan {
static func resolveFrontTabId(remainingTabIds: [UUID], firstTabId: UUID, selectedId: UUID?) -> UUID {
guard let selectedId else { return firstTabId }
if selectedId == firstTabId { return firstTabId }
if remainingTabIds.contains(selectedId) { return selectedId }
return firstTabId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
)
window.identifier = NSUserInterfaceItemIdentifier("main")
window.minSize = NSSize(width: 720, height: 480)
window.isRestorable = AppSettingsStorage.shared.loadGeneral().startupBehavior == .reopenLast
window.restorationClass = TabWindowRestoration.self
window.isRestorable = false
window.toolbarStyle = .unified
window.titleVisibility = .visible
window.tabbingMode = .preferred
Expand Down Expand Up @@ -88,11 +87,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
fatalError("TabWindowController does not support NSCoder init")
}

override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(payload.connectionId.uuidString as NSString, forKey: TabWindowRestoration.connectionIdKey)
}

// MARK: - NSWindowDelegate

internal func windowDidResize(_ notification: Notification) {
Expand Down
81 changes: 0 additions & 81 deletions TablePro/Core/Services/Infrastructure/TabWindowRestoration.swift

This file was deleted.

16 changes: 11 additions & 5 deletions TablePro/Core/Services/Infrastructure/WindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ internal final class WindowManager {

// MARK: - Open

internal func openTab(payload: EditorTabPayload) {
internal func openTab(payload: EditorTabPayload, activate: Bool = true) {
let t0 = Date()
Self.lifecycleLogger.info(
"[open] WindowManager.openTab start payloadId=\(payload.id, privacy: .public) connId=\(payload.connectionId, privacy: .public) intent=\(String(describing: payload.intent), privacy: .public) skipAutoExecute=\(payload.skipAutoExecute)"
"[open] WindowManager.openTab start payloadId=\(payload.id, privacy: .public) connId=\(payload.connectionId, privacy: .public) intent=\(String(describing: payload.intent), privacy: .public) skipAutoExecute=\(payload.skipAutoExecute) activate=\(activate)"
)

let resolvedConnection = DatabaseManager.shared.activeSessions[payload.connectionId]?.connection
Expand Down Expand Up @@ -66,13 +66,19 @@ internal final class WindowManager {
}
let target = sibling.tabbedWindows?.last ?? sibling
target.addTabbedWindow(window, ordered: .above)
window.makeKeyAndOrderFront(nil)
if activate {
window.makeKeyAndOrderFront(nil)
}
Self.lifecycleLogger.info(
"[open] WindowManager joined existing tab group payloadId=\(payload.id, privacy: .public) tabbingId=\(tabbingId, privacy: .public)"
)
} else {
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
if activate {
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
} else {
window.orderFront(nil)
}
Self.lifecycleLogger.info(
"[open] WindowManager standalone window payloadId=\(payload.id, privacy: .public) tabbingId=\(tabbingId, privacy: .public)"
)
Expand Down
15 changes: 15 additions & 0 deletions TablePro/Models/Query/TableLoadTrigger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// TableLoadTrigger.swift
// TablePro
//

import Foundation

internal enum TableLoadTrigger {
case userInitiated
case restore

var suppressesFailureModal: Bool {
self == .restore
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,19 @@ extension MainContentCoordinator {
_ error: Error,
sql: String,
tabId: UUID,
connection conn: DatabaseConnection
connection conn: DatabaseConnection,
trigger: TableLoadTrigger = .userInitiated
) {
queryExecutionCoordinator.handleQueryExecutionError(
error,
sql: sql,
tabId: tabId,
connection: conn
connection: conn,
trigger: trigger
)
}

func restoreSchemaAndRunQuery(_ schema: String) async {
await queryExecutionCoordinator.restoreSchemaAndRunQuery(schema)
func restoreSchemaAndRunQuery(_ schema: String, trigger: TableLoadTrigger = .userInitiated) async {
await queryExecutionCoordinator.restoreSchemaAndRunQuery(schema, trigger: trigger)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import Foundation
import TableProPluginKit

extension MainContentCoordinator {
func openTableTabQuery(tabId: UUID) async {
func openTableTabQuery(tabId: UUID, trigger: TableLoadTrigger = .userInitiated) async {
guard await prepareTableTabFirstLoad(tabId: tabId) else { return }
executeTableTabQueryDirectly()
executeTableTabQueryDirectly(trigger: trigger)
}

@discardableResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ extension MainContentCoordinator {

/// Called from `TabWindowController.windowDidBecomeKey(_:)`.
/// Updates focus state, refreshes file-based schema if stale, and syncs the
/// sidebar selection to the active tab. No query work runs here — lazy-load
/// is owned by `MainEditorContentView`'s `.task(id:)` modifier.
/// sidebar selection to the active tab. The one query-related action here is
/// consuming a deferred restore load: a restored background tab loads its data
/// the first time its window becomes key. All other lazy-load is owned by
/// `MainEditorContentView`'s `.task(id:)` modifier.
func handleWindowDidBecomeKey() {
let t0 = Date()
Self.lifecycleLogger.debug(
Expand All @@ -29,6 +31,8 @@ extension MainContentCoordinator {
evictionTask?.cancel()
evictionTask = nil

consumeDeferredRestoreLoadIfNeeded()

syncSidebarToSelectedTab()
announceActiveTabToVoiceOver()

Expand Down Expand Up @@ -133,16 +137,17 @@ extension MainContentCoordinator {

// MARK: - Lazy Load

func lazyLoadCurrentTabIfNeeded() {
func lazyLoadCurrentTabIfNeeded(trigger: TableLoadTrigger = .userInitiated) {
guard let tab = tabManager.selectedTab else { return }
guard deferredRestoreLoadTabId != tab.id else { return }
guard canAutoLoadTableTab(tab) else { return }
guard tableLoadTasks[tab.id] == nil else { return }

clearAbandonedExecutingFlagIfNeeded(for: tab)

guard let session = DatabaseManager.shared.session(for: connectionId),
session.isConnected else {
needsLazyLoad = true
pendingLoadTrigger = trigger
return
}

Expand All @@ -158,7 +163,7 @@ extension MainContentCoordinator {
self.tableLoadTasks[tabId] = nil
}
}
await self.openTableTabQuery(tabId: tabId)
await self.openTableTabQuery(tabId: tabId, trigger: trigger)
if let queryTask = self.currentQueryTask {
await queryTask.value
}
Expand All @@ -171,6 +176,14 @@ extension MainContentCoordinator {
tableLoadTasks[tabId] = nil
}

func consumeDeferredRestoreLoadIfNeeded() {
guard isKeyWindow else { return }
guard let deferredId = deferredRestoreLoadTabId,
deferredId == tabManager.selectedTabId else { return }
deferredRestoreLoadTabId = nil
lazyLoadCurrentTabIfNeeded(trigger: .restore)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore schema before loading deferred tabs

When a restored background table tab becomes key after the connection is already established, this new deferred path goes straight to lazyLoadCurrentTabIfNeeded. Unlike the pending-load path in MainContentView+Helpers.consumePendingLoad, it never compares the tab's saved schemaName with session.currentSchema or calls restoreSchemaAndRunQuery, so a PostgreSQL tab restored from a non-current schema can run its first load and metadata/row-count fetches under the schema left active by another restored tab. Route deferred restore consumption through the same database/schema restoration step before loading.

Useful? React with 👍 / 👎.

}

private func canAutoLoadTableTab(_ tab: QueryTab) -> Bool {
guard tab.tabType == .table else { return false }
guard tab.execution.errorMessage == nil else { return false }
Expand Down
Loading
Loading