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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-window tab persistence dropped all but one tab on relaunch. Three save paths each wrote only the current window's tabs, racing the `willTerminate` aggregate save: `handleTabSelectionChange` (every time you switched tabs), the `mainWindowWillClose` notification observer, and the per-window `handleWindowWillClose` close path. On Cmd+Q, AppKit closes windows LIFO so the last writer left only the first-opened window's tab in the file. The tab-selection path now writes aggregated tabs from all windows of the connection. Per-window close-time saves no-op when the app is terminating, leaving the canonical `willTerminate` aggregate save intact. The `mainWindowWillClose` observer also routes through the aggregated save path.
- Filter value autocomplete popover stole keyboard focus from the text field after the first keystroke when Full Keyboard Access was enabled (System Settings → Keyboard → Keyboard Navigation). The popover content used SwiftUI `Button` rows, which become focus targets under FKA, so SwiftUI auto-focused the first row when the popover appeared. Replaced the rows with `Text` + `.onTapGesture` (non-focusable) and marked the dropdown as `.focusable(false)`.
- Toolbar database name was empty after relaunching with a connection that had no database configured but a last-used database restored via `selectDatabaseFromLastSession`. The window opened (and the toolbar resolved its initial name) before the post-connect actions populated `session.currentDatabase`, so the toolbar fell back to the empty `connection.database`. Sidebar and Cmd+K both worked because they read the session directly. The toolbar now re-syncs its database name on every `connectionStatusDidChange`, picking up the restored value once the session settles.
- After switching databases with Cmd+K, several flows silently snapped back to the connection's saved default. The visible symptom was Cmd+T opening a new query tab on the wrong database (the new window flashed on the active database and reverted within a frame). Less visible variants of the same root cause: query history rows recorded against the saved default instead of the actually-targeted database; the column-types cache keyed on the saved default so two databases with same-named tables collided after a switch; AI prompts and autocomplete schema context told the model "the database is X" while the user was working in Y; and several tab-creation paths (Create Table, Create View, Server Dashboard, Run Favorite, Open Linked SQL Favorite, Show All Tables for Mongo/Redis) inherited the same wrong-database label and triggered the same flash-and-revert through `MainContentCoordinator.handleTabChange`'s active-database reconciliation. The codebase had no canonical accessor for "active database for this connection" and every call site re-derived it inline against `connection.database` (the saved default). Two new helpers replace the inline lookups: `DatabaseManager.activeDatabaseName(for: DatabaseConnection)` for service-layer code without a coordinator (ImportService, AIChatViewModel, SQLSchemaProvider, CreateTableView, TableStructureView, SessionStateFactory, MainContentCommandActions, TerminalTabContentView), and `MainContentCoordinator.activeDatabaseName` for coordinator extensions (Navigation, TabSwitch, FKNavigation, ERDiagram, Terminal, ServerDashboard, SidebarActions, Favorites, QueryHelpers, MultiStatement, SaveChanges, QueryParameters). Both resolve to `session.activeDatabase ?? connection.database`, so flows that ran before any session existed still get the saved default. Fixes #1043.
- AI provider settings showed `unsupported URL` on Test Connection and Reload Models while editing a draft provider's endpoint. `AIProviderFactory` cached the created provider by `(id, apiKey)` only, so a debounced model fetch that fired during a momentarily empty endpoint (cleared before retyping) stashed a provider built with `endpoint = ""`. Subsequent calls under the same draft id returned that stale instance regardless of the now-correct endpoint, and `URL(string: "/v1/models")` was rejected with `URLError.unsupportedURL`. Cache invalidation only happened on save. The cache now compares the full `AIProviderConfig` along with `apiKey`, so any field change rebuilds the provider.

### Changed
Expand Down
7 changes: 4 additions & 3 deletions TablePro/Core/Autocomplete/SQLSchemaProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,14 @@ actor SQLSchemaProvider {
}

let dbType = connection.type
let dbName = connection.database
let capturedConnection = connection
let capturedTables = tables
let (idQuote, editorLanguage, queryLanguageName) = await MainActor.run {
let (dbName, idQuote, editorLanguage, queryLanguageName) = await MainActor.run {
let resolvedName = DatabaseManager.shared.activeDatabaseName(for: capturedConnection)
let quote = PluginManager.shared.sqlDialect(for: dbType)?.identifierQuote ?? "\""
let lang = PluginManager.shared.editorLanguage(for: dbType)
let langName = PluginManager.shared.queryLanguageName(for: dbType)
return (quote, lang, langName)
return (resolvedName, quote, lang, langName)
}

return AISchemaContext.buildSystemPrompt(
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Database/DatabaseManager+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ extension DatabaseManager {

// Record each statement in query history
let connId = connectionId
let dbName = self.activeSessions[connectionId]?.connection.database ?? ""
let dbName = self.activeSessions[connectionId]?.activeDatabase ?? ""
for stmt in statements {
QueryHistoryManager.shared.recordQuery(
query: stmt.sql.hasSuffix(";") ? stmt.sql : stmt.sql + ";",
Expand Down
7 changes: 7 additions & 0 deletions TablePro/Core/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ final class DatabaseManager {
activeSessions[connectionId]
}

/// Authoritative active database for this connection. Use for tab payloads,
/// query history, schema cache keys, and AI prompt context. Reading
/// `connection.database` (the saved default) is wrong after Cmd+K.
func activeDatabaseName(for connection: DatabaseConnection) -> String {
activeSessions[connection.id]?.activeDatabase ?? connection.database
}

/// Current connection status
var status: ConnectionStatus {
currentSession?.status ?? .disconnected
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Core/Services/Export/ImportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ final class ImportService {
QueryHistoryManager.shared.recordQuery(
query: "-- Import from \(url.lastPathComponent) (\(progress.processedStatements) statements before failure)",
connectionId: connection.id,
databaseName: connection.database,
databaseName: DatabaseManager.shared.activeDatabaseName(for: connection),
executionTime: 0,
rowCount: progress.processedStatements,
wasSuccessful: false,
Expand All @@ -140,7 +140,7 @@ final class ImportService {
QueryHistoryManager.shared.recordQuery(
query: "-- Import from \(url.lastPathComponent) (\(result.executedStatements) statements)",
connectionId: connection.id,
databaseName: connection.database,
databaseName: DatabaseManager.shared.activeDatabaseName(for: connection),
executionTime: result.executionTime,
rowCount: result.executedStatements,
wasSuccessful: true,
Expand Down
20 changes: 11 additions & 9 deletions TablePro/Core/Services/Infrastructure/SessionStateFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ enum SessionStateFactory {
toolbarSt.databaseName = String(dbIndex)
}

let activeDatabaseName = DatabaseManager.shared.activeDatabaseName(for: connection)

if let payload {
switch payload.intent {
case .openContent:
Expand All @@ -94,13 +96,13 @@ enum SessionStateFactory {
try tabMgr.addPreviewTableTab(
tableName: tableName,
databaseType: connection.type,
databaseName: payload.databaseName ?? connection.database
databaseName: payload.databaseName ?? activeDatabaseName
)
} else {
try tabMgr.addTableTab(
tableName: tableName,
databaseType: connection.type,
databaseName: payload.databaseName ?? connection.database
databaseName: payload.databaseName ?? activeDatabaseName
)
}
} catch {
Expand All @@ -118,29 +120,29 @@ enum SessionStateFactory {
}
}
} else {
tabMgr.addTab(databaseName: payload.databaseName ?? connection.database)
tabMgr.addTab(databaseName: payload.databaseName ?? activeDatabaseName)
}
case .query:
tabMgr.addTab(
initialQuery: payload.initialQuery,
title: payload.tabTitle,
databaseName: payload.databaseName ?? connection.database,
databaseName: payload.databaseName ?? activeDatabaseName,
sourceFileURL: payload.sourceFileURL
)
case .createTable:
tabMgr.addCreateTableTab(
databaseName: payload.databaseName ?? connection.database
databaseName: payload.databaseName ?? activeDatabaseName
)
case .erDiagram:
tabMgr.addERDiagramTab(
schemaKey: payload.erDiagramSchemaKey ?? payload.databaseName ?? connection.database,
databaseName: payload.databaseName ?? connection.database
schemaKey: payload.erDiagramSchemaKey ?? payload.databaseName ?? activeDatabaseName,
databaseName: payload.databaseName ?? activeDatabaseName
)
case .serverDashboard:
tabMgr.addServerDashboardTab()
case .terminal:
tabMgr.addTerminalTab(
databaseName: payload.databaseName ?? connection.database
databaseName: payload.databaseName ?? activeDatabaseName
)
}
case .newEmptyTab:
Expand All @@ -149,7 +151,7 @@ enum SessionStateFactory {
tabMgr.addTab(
initialQuery: payload.initialQuery,
title: title,
databaseName: payload.databaseName ?? connection.database
databaseName: payload.databaseName ?? activeDatabaseName
)
case .restoreOrDefault:
break
Expand Down
2 changes: 1 addition & 1 deletion TablePro/ViewModels/AIChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ final class AIChatViewModel {
guard let connection else { return nil }
return PromptContext(
databaseType: connection.type,
databaseName: connection.database,
databaseName: DatabaseManager.shared.activeDatabaseName(for: connection),
tables: tables,
columnsByTable: columnsByTable,
foreignKeys: foreignKeysByTable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ extension MainContentCoordinator {
/// 3. Otherwise open a new native window tab so the current tab's content
/// (unsaved queries, filters, etc.) is preserved.
func showERDiagram() {
let session = DatabaseManager.shared.session(for: connectionId)
let dbName = session?.activeDatabase ?? connection.database
let schemaName = session?.currentSchema
let dbName = activeDatabaseName
let schemaName = DatabaseManager.shared.session(for: connectionId)?.currentSchema
let schemaKey = "\(dbName).\(schemaName ?? "default")"

if let existing = Self.coordinator(forConnection: connectionId, tabMatching: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,7 @@ extension MainContentCoordinator {
value: value
)

// Get current database context
let currentDatabase: String
if let session = DatabaseManager.shared.session(for: connectionId) {
currentDatabase = session.activeDatabase
} else {
currentDatabase = connection.database
}
let currentDatabase = activeDatabaseName

let targetSchema = fkInfo.referencedSchema ?? DatabaseManager.shared.session(for: connectionId)?.currentSchema

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extension MainContentCoordinator {
let payload = EditorTabPayload(
connectionId: connection.id,
tabType: .query,
databaseName: connection.database,
databaseName: activeDatabaseName,
initialQuery: loaded.content,
sourceFileURL: favorite.fileURL,
tabTitle: favorite.name
Expand Down Expand Up @@ -118,7 +118,7 @@ extension MainContentCoordinator {
let payload = EditorTabPayload(
connectionId: connection.id,
tabType: .query,
databaseName: connection.database,
databaseName: activeDatabaseName,
initialQuery: favorite.query
)
WindowManager.shared.openTab(payload: payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: historySQL,
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: result.executionTime,
rowCount: result.rows.count,
wasSuccessful: true,
Expand Down Expand Up @@ -176,7 +176,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: recordSQL,
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: cumulativeTime,
rowCount: 0,
wasSuccessful: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,14 @@ extension MainContentCoordinator {
forTypeId: connection.type.pluginTypeId
)?.navigationModel ?? .standard

// Get current database name from active session (may differ from connection default after Cmd+K switch)
let currentDatabase: String
if navigationModel == .inPlace {
// In-place navigation: extract db index from table name "db3" → "3"
guard tableName.hasPrefix("db"), Int(String(tableName.dropFirst(2))) != nil else {
return
}
currentDatabase = String(tableName.dropFirst(2))
} else if let session = DatabaseManager.shared.session(for: connectionId) {
currentDatabase = session.activeDatabase
} else {
currentDatabase = connection.database
currentDatabase = activeDatabaseName
}

let currentSchema = DatabaseManager.shared.session(for: connectionId)?.currentSchema
Expand Down Expand Up @@ -366,14 +362,14 @@ extension MainContentCoordinator {
if editorLang == .javascript {
tabManager.addTab(
initialQuery: "db.runCommand({\"listCollections\": 1, \"nameOnly\": false})",
databaseName: connection.database
databaseName: activeDatabaseName
)
runQuery()
return nil
} else if editorLang == .bash {
tabManager.addTab(
initialQuery: "SCAN 0 MATCH * COUNT 100",
databaseName: connection.database
databaseName: activeDatabaseName
)
runQuery()
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ extension MainContentCoordinator {
// Cache column types for selective queries on subsequent page/filter/sort reloads.
// Only cache from schema-backed table loads (not arbitrary SELECTs which may have partial columns).
if let tbl = tableName, !tbl.isEmpty, hasSchema {
let cacheKey = "\(conn.id):\(conn.database):\(tbl)"
let cacheKey = "\(conn.id):\(activeDatabaseName):\(tbl)"
cachedTableColumnTypes[cacheKey] = columnTypes
cachedTableColumnNames[cacheKey] = columns
}
Expand Down Expand Up @@ -187,7 +187,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: sql,
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: executionTime,
rowCount: rows.count,
wasSuccessful: true,
Expand Down Expand Up @@ -405,7 +405,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: sql,
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: 0,
rowCount: 0,
wasSuccessful: false,
Expand Down Expand Up @@ -463,7 +463,7 @@ extension MainContentCoordinator {
/// Build column exclusions for a table using cached column type info.
/// Returns empty if no cached types exist (first load uses SELECT *).
func columnExclusions(for tableName: String) -> [ColumnExclusion] {
let cacheKey = "\(connectionId):\(connection.database):\(tableName)"
let cacheKey = "\(connectionId):\(activeDatabaseName):\(tableName)"
guard let cachedTypes = cachedTableColumnTypes[cacheKey],
let cachedCols = cachedTableColumnNames[cacheKey] else {
return []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: historySQL,
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: result.executionTime,
rowCount: result.rows.count,
wasSuccessful: true,
Expand Down Expand Up @@ -449,7 +449,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: recordSQL,
connectionId: connection.id,
databaseName: connection.database,
databaseName: activeDatabaseName,
executionTime: cumulativeTime,
rowCount: 0,
wasSuccessful: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: historySQL.hasSuffix(";") ? historySQL : historySQL + ";",
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: executionTime,
rowCount: 0,
wasSuccessful: true,
Expand Down Expand Up @@ -288,7 +288,7 @@ extension MainContentCoordinator {
QueryHistoryManager.shared.recordQuery(
query: allSQL,
connectionId: conn.id,
databaseName: conn.database,
databaseName: activeDatabaseName,
executionTime: executionTime,
rowCount: 0,
wasSuccessful: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension MainContentCoordinator {
let payload = EditorTabPayload(
connectionId: connection.id,
tabType: .serverDashboard,
databaseName: connection.database
databaseName: activeDatabaseName
)
WindowManager.shared.openTab(payload: payload)
}
Expand Down
Loading
Loading