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
- iOS: running a query that returns a very large result no longer crashes the app. The query editor keeps the first rows it loads, stops before memory runs low, and tells you to add LIMIT to fetch more.
- iOS: Safe Mode "Confirm Writes" now prompts before saving a row edit or inserting a row, matching the query editor. Previously grid edits and inserts saved with no confirmation.
- Redshift: schema switching now works, along with the contains, starts with, and ends with filters and table search. All previously failed with a SQL syntax error. (#1439)
- Opening a `.sql` file now names the tab after the file instead of showing "SQL Query". (#1220)

## [0.45.0] - 2026-05-26

Expand Down
55 changes: 36 additions & 19 deletions TablePro/Core/Services/Infrastructure/MainSplitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

private var connectionStatusCancellable: AnyCancellable?

// MARK: - Title Resolution

static func resolveDefaultTitle(payload: EditorTabPayload?, queryLanguageName: String?) -> String {
switch payload?.tabType {
case .serverDashboard:
return String(localized: "Server Dashboard")
case .erDiagram:
return String(localized: "ER Diagram")
case .createTable:
return String(localized: "Create Table")
default:
break
}
if let tabTitle = payload?.tabTitle {
return tabTitle
}
if let sourceFileURL = payload?.sourceFileURL {
return QueryTab.fileDisplayTitle(for: sourceFileURL)
}
if let tableName = payload?.tableName {
return tableName
}
if let queryLanguageName {
return String(format: String(localized: "%@ Query"), queryLanguageName)
}
return String(localized: "SQL Query")
}

// MARK: - Init

init(payload: EditorTabPayload?, sessionState: SessionStateFactory.SessionState?) {
Expand All @@ -60,25 +88,14 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
self.payloadConnection = nil
}

let defaultTitle: String
if payload?.tabType == .serverDashboard {
defaultTitle = String(localized: "Server Dashboard")
} else if payload?.tabType == .erDiagram {
defaultTitle = String(localized: "ER Diagram")
} else if payload?.tabType == .createTable {
defaultTitle = String(localized: "Create Table")
} else if let tabTitle = payload?.tabTitle {
defaultTitle = tabTitle
} else if let tableName = payload?.tableName {
defaultTitle = tableName
} else if let connectionId = payload?.connectionId,
let connection = DatabaseManager.shared.activeSessions[connectionId]?.connection {
let langName = PluginManager.shared.queryLanguageName(for: connection.type)
defaultTitle = "\(langName) Query"
} else {
defaultTitle = String(localized: "SQL Query")
}
self.windowTitle = defaultTitle
let queryLanguageName: String? = {
guard let connectionId = payload?.connectionId,
let connection = DatabaseManager.shared.activeSessions[connectionId]?.connection else {
return nil
}
return PluginManager.shared.queryLanguageName(for: connection.type)
}()
self.windowTitle = Self.resolveDefaultTitle(payload: payload, queryLanguageName: queryLanguageName)

var resolvedSession: ConnectionSession?
if let connectionId = payload?.connectionId {
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Models/Query/QueryTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ struct QueryTab: Identifiable, Equatable {
}
}

static func fileDisplayTitle(for url: URL) -> String {
FileManager.default.displayName(atPath: url.path(percentEncoded: false))
}

var hasUserActiveSort: Bool {
sortState.isSorting && sortState.source == .user
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Models/Query/QueryTabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ final class QueryTabManager {
if let title {
tabTitle = title
} else if let sourceFileURL {
tabTitle = sourceFileURL.deletingPathExtension().lastPathComponent
tabTitle = QueryTab.fileDisplayTitle(for: sourceFileURL)
} else {
tabTitle = nextTitle()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ extension MainContentCoordinator {
if tabManager.tabs.isEmpty {
tabManager.addTab(
initialQuery: loaded.content,
title: favorite.name,
sourceFileURL: favorite.fileURL
)
registerWindowForSourceFile(favorite.fileURL)
Expand All @@ -70,7 +69,7 @@ extension MainContentCoordinator {
tab.content.query = loaded.content
tab.content.savedFileContent = loaded.content
tab.content.loadMtime = mtime
tab.title = favorite.name
tab.title = QueryTab.fileDisplayTitle(for: favorite.fileURL)
}
registerWindowForSourceFile(favorite.fileURL)
return
Expand All @@ -81,8 +80,7 @@ extension MainContentCoordinator {
tabType: .query,
databaseName: activeDatabaseName,
initialQuery: loaded.content,
sourceFileURL: favorite.fileURL,
tabTitle: favorite.name
sourceFileURL: favorite.fileURL
)
WindowManager.shared.openTab(payload: payload)
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Main/Extensions/MainContentView+Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ extension MainContentView {
} else if selectedTab?.tabType == .erDiagram {
windowTitle = String(localized: "ER Diagram")
} else if let fileURL = selectedTab?.content.sourceFileURL {
windowTitle = selectedTab?.title ?? fileURL.deletingPathExtension().lastPathComponent
windowTitle = selectedTab?.title ?? QueryTab.fileDisplayTitle(for: fileURL)
} else {
let langName = PluginManager.shared.queryLanguageName(for: connection.type)
let queryLabel = String(format: String(localized: "%@ Query"), langName)
Expand Down
15 changes: 15 additions & 0 deletions TableProTests/Models/EditorTabPayloadTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,25 @@ struct EditorTabPayloadTests {
let payload = EditorTabPayload(from: tab, connectionId: connectionId)
#expect(payload.connectionId == connectionId)
#expect(payload.tabType == tab.tabType)
#expect(payload.tabTitle == tab.title)
#expect(payload.tableName == tab.tableContext.tableName)
#expect(payload.databaseName == tab.tableContext.databaseName)
#expect(payload.initialQuery == tab.content.query)
#expect(payload.isView == tab.tableContext.isView)
#expect(payload.showStructure == (tab.display.resultsViewMode == .structure))
}

@Test("Init from a file-backed query tab preserves the file title")
@MainActor
func initFromFileBackedQueryTab() throws {
let tabManager = QueryTabManager()
let url = URL(fileURLWithPath: "/tmp/report.sql")
tabManager.addTab(initialQuery: "SELECT 1", sourceFileURL: url)
let tab = try #require(tabManager.tabs.first)
let connectionId = UUID()
let payload = EditorTabPayload(from: tab, connectionId: connectionId)
#expect(payload.sourceFileURL == url)
#expect(payload.tabTitle == tab.title)
#expect(payload.tabTitle == QueryTab.fileDisplayTitle(for: url))
}
}
156 changes: 156 additions & 0 deletions TableProTests/Services/MainSplitViewControllerTitleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import Foundation
@testable import TablePro
import Testing

@Suite("MainSplitViewController.resolveDefaultTitle")
@MainActor
struct MainSplitViewControllerTitleTests {
@Test("Nil payload falls back to SQL Query")
func nilPayloadFallsBackToSQLQuery() {
let title = MainSplitViewController.resolveDefaultTitle(payload: nil, queryLanguageName: nil)
#expect(title == String(localized: "SQL Query"))
}

@Test("Server dashboard payload returns Server Dashboard")
func serverDashboardLabel() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .serverDashboard)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == String(localized: "Server Dashboard"))
}

@Test("ER diagram payload returns ER Diagram")
func erDiagramLabel() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .erDiagram)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == String(localized: "ER Diagram"))
}

@Test("Create table payload returns Create Table")
func createTableLabel() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .createTable)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == String(localized: "Create Table"))
}

@Test("Explicit tabTitle wins")
func explicitTabTitleWins() {
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .query,
tabTitle: "report"
)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == "report")
}

@Test("Source file URL resolves to file display name, not language fallback")
func sourceFileURLBeatsLanguageFallback() {
let url = URL(fileURLWithPath: "/tmp/report.sql")
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .query,
sourceFileURL: url
)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == QueryTab.fileDisplayTitle(for: url))
#expect(title != "PostgreSQL Query")
#expect(title != String(localized: "SQL Query"))
}

@Test("Explicit tabTitle takes precedence over sourceFileURL")
func tabTitlePrecedesSourceFileURL() {
let url = URL(fileURLWithPath: "/tmp/report.sql")
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .query,
sourceFileURL: url,
tabTitle: "Renamed"
)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == "Renamed")
}

@Test("Source file URL takes precedence over tableName")
func sourceFileURLPrecedesTableName() {
let url = URL(fileURLWithPath: "/tmp/report.sql")
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .query,
tableName: "users",
sourceFileURL: url
)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == QueryTab.fileDisplayTitle(for: url))
}

@Test("Table payload with tableName returns the table name")
func tableNameUsedForTablePayload() {
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .table,
tableName: "users"
)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == "users")
}

@Test("Query payload with language name uses localized language label")
func queryWithLanguageFallback() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .query)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: "PostgreSQL")
#expect(title == String(format: String(localized: "%@ Query"), "PostgreSQL"))
}

@Test("Query payload with no language name falls back to SQL Query")
func queryWithoutLanguageFallback() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .query)
let title = MainSplitViewController.resolveDefaultTitle(payload: payload, queryLanguageName: nil)
#expect(title == String(localized: "SQL Query"))
}
}

@Suite("QueryTab.fileDisplayTitle")
struct QueryTabFileDisplayTitleTests {
@Test("Returns FileManager display name for the URL")
func returnsFileManagerDisplayName() {
let url = URL(fileURLWithPath: "/tmp/report.sql")
let title = QueryTab.fileDisplayTitle(for: url)
#expect(title == FileManager.default.displayName(atPath: url.path(percentEncoded: false)))
}

@Test("Strips directory components")
func stripsDirectoryComponents() {
let url = URL(fileURLWithPath: "/var/folders/xyz/queries/report.sql")
let title = QueryTab.fileDisplayTitle(for: url)
#expect(!title.contains("/"))
}

@Test("Non-empty result for a file URL")
func nonEmptyResult() {
let url = URL(fileURLWithPath: "/tmp/report.sql")
let title = QueryTab.fileDisplayTitle(for: url)
#expect(!title.isEmpty)
}
}

@Suite("QueryTabManager.addTab with sourceFileURL")
@MainActor
struct QueryTabManagerAddTabSourceFileTests {
@Test("Tab title uses the shared file display title helper")
func tabTitleUsesSharedHelper() {
let tabManager = QueryTabManager()
let url = URL(fileURLWithPath: "/tmp/report.sql")
tabManager.addTab(sourceFileURL: url)
let tab = tabManager.tabs.first
#expect(tab?.title == QueryTab.fileDisplayTitle(for: url))
}

@Test("Explicit title argument wins over sourceFileURL")
func explicitTitleWinsOverSourceFileURL() {
let tabManager = QueryTabManager()
let url = URL(fileURLWithPath: "/tmp/report.sql")
tabManager.addTab(title: "favorite-name", sourceFileURL: url)
let tab = tabManager.tabs.first
#expect(tab?.title == "favorite-name")
}
}
Loading