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 @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The database switcher takes a multiple selection too, with the same actions on the right-click menu.
- Drop Schema for PostgreSQL, SQL Server and SurrealDB.
- Quick Switcher can search tables and views across every open connection.
- Quick Switcher can search saved queries and recent queries across every open connection.
- SSL settings on mobile for MySQL, PostgreSQL and Redis: a mode picker plus CA, client certificate and client key. Import a PEM file, paste one, or use a PKCS#12 file. (#2083)
- Legacy UUID Encoding on a MongoDB connection, so binary UUIDs written by the Java, C# or Python drivers read as UUIDs instead of hex. Filters, edits and MQL exports write the same bytes back. (#2086)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// QuickSwitcherHostResolver.swift
// TablePro
//

import Foundation

internal enum QuickSwitcherHostResolver {
/// Which window opens a quick switcher result, given every window that could host it.
///
/// The window the panel was opened from wins whenever it can host the result. Every other
/// window on the same connection and database is an equally good match, and the registry
/// holding them is a dictionary with no order, so picking one arbitrarily loads the result
/// into an editor the user is not looking at and pulls focus there. When the result belongs
/// to another connection, the most recently focused window of that connection wins.
internal static func host<ID: Equatable>(
preferred: ID?,
candidates: [ID],
mostRecentlyFocused: ID?
) -> ID? {
if let preferred, candidates.contains(preferred) { return preferred }
if let mostRecentlyFocused, candidates.contains(mostRecentlyFocused) { return mostRecentlyFocused }
return candidates.first
}
}
10 changes: 8 additions & 2 deletions TablePro/Core/Storage/SQLFavoriteManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,15 @@ internal final class SQLFavoriteManager: @unchecked Sendable {
func fetchFavorites(
connectionId: UUID? = nil,
folderId: UUID? = nil,
searchText: String? = nil
searchText: String? = nil,
allowedConnectionIds: Set<UUID>? = nil
) async -> [SQLFavorite] {
await storage.fetchFavorites(connectionId: connectionId, folderId: folderId, searchText: searchText)
await storage.fetchFavorites(
connectionId: connectionId,
folderId: folderId,
searchText: searchText,
allowedConnectionIds: allowedConnectionIds
)
}

// MARK: - Folders
Expand Down
29 changes: 28 additions & 1 deletion TablePro/Core/Storage/SQLFavoriteStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -541,15 +541,25 @@ internal actor SQLFavoriteStorage {
func fetchFavorites(
connectionId: UUID? = nil,
folderId: UUID? = nil,
searchText: String? = nil
searchText: String? = nil,
allowedConnectionIds: Set<UUID>? = nil
) -> [SQLFavorite] {
if let allowedConnectionIds, allowedConnectionIds.isEmpty {
return []
}

let connectionIdString = connectionId?.uuidString
let folderIdString = folderId?.uuidString
let allowedList = allowedConnectionIds.map { Array($0) }
let allowedPlaceholders = allowedList.map {
Array(repeating: "?", count: $0.count).joined(separator: ", ")
}

var sql: String
var bindIndex: Int32 = 1
var hasConnectionFilter = false
var hasFolderFilter = false
var hasAllowedFilter = false

let isJoined: Bool
if let searchText = searchText, !searchText.isEmpty {
Expand All @@ -566,6 +576,11 @@ internal actor SQLFavoriteStorage {
hasConnectionFilter = true
}

if let allowedPlaceholders {
sql += " AND (f.connection_id IS NULL OR f.connection_id IN (\(allowedPlaceholders)))"
hasAllowedFilter = true
}

if folderIdString != nil {
sql += " AND f.folder_id = ?"
hasFolderFilter = true
Expand All @@ -584,6 +599,11 @@ internal actor SQLFavoriteStorage {
hasConnectionFilter = true
}

if let allowedPlaceholders {
whereClauses.append("(connection_id IS NULL OR connection_id IN (\(allowedPlaceholders)))")
hasAllowedFilter = true
}

if folderIdString != nil {
whereClauses.append("folder_id = ?")
hasFolderFilter = true
Expand Down Expand Up @@ -616,6 +636,13 @@ internal actor SQLFavoriteStorage {
bindIndex += 1
}

if let allowedList, hasAllowedFilter {
for allowedId in allowedList {
sqlite3_bind_text(statement, bindIndex, allowedId.uuidString, -1, SQLITE_TRANSIENT)
bindIndex += 1
}
}

if let foldId = folderIdString, hasFolderFilter {
sqlite3_bind_text(statement, bindIndex, foldId, -1, SQLITE_TRANSIENT)
bindIndex += 1
Expand Down
6 changes: 6 additions & 0 deletions TablePro/Models/Query/QueryTabState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ struct TabTableContext: Equatable {
var isView: Bool = false

var primaryKeyColumn: String? { primaryKeyColumns.first }

/// A tab opened without an explicit database carries an empty name and follows the window's
/// browse cursor, so comparing the stored value against a real database name never matches.
func resolvedDatabaseName(browsing browseDatabaseName: String) -> String {
databaseName.isEmpty ? browseDatabaseName : databaseName
}
}

struct TabQueryContent: Equatable {
Expand Down
16 changes: 13 additions & 3 deletions TablePro/Models/UI/QuickSwitcherItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import TableProPluginKit

/// The type of database object represented by a quick switcher item
internal enum QuickSwitcherItemKind: String, Hashable, Sendable {
Expand All @@ -25,25 +26,28 @@ internal enum QuickSwitcherCommitIntent: Sendable {
case openStructure
}

internal struct QuickSwitcherObjectTarget: Hashable, Sendable {
internal struct QuickSwitcherTarget: Hashable, Sendable {
let connectionId: UUID
let connectionName: String
let databaseName: String?
let schemaName: String?
let databaseDisplayName: String?
let pathFieldRole: PathFieldRole

init(
connectionId: UUID,
connectionName: String,
databaseName: String?,
schemaName: String?,
databaseDisplayName: String? = nil
databaseDisplayName: String? = nil,
pathFieldRole: PathFieldRole = .database
) {
self.connectionId = connectionId
self.connectionName = connectionName
self.databaseName = databaseName
self.schemaName = schemaName
self.databaseDisplayName = databaseDisplayName
self.pathFieldRole = pathFieldRole
}
}

Expand All @@ -61,6 +65,8 @@ internal enum QuickSwitcherScope: String, CaseIterable, Identifiable, Sendable {
/// from the objects of the connection that opened the panel.
var usesCrossConnectionCatalog: Bool { self == .connections }

var usesCrossConnectionQueries: Bool { self == .queries }

var includedKinds: Set<QuickSwitcherItemKind>? {
switch self {
case .all: return nil
Expand Down Expand Up @@ -98,11 +104,15 @@ internal struct QuickSwitcherItem: Identifiable, Hashable, Sendable {
let name: String
let kind: QuickSwitcherItemKind
let subtitle: String
/// Ranked at full weight, unlike the subtitle that carries it for display. A saved query's
/// subtitle also names its connection and database, and those must not score as strongly as
/// the keyword the author assigned.
var keyword: String?
var matchedIndices: [Int] = []
var payload: String?
var isOpenInTab: Bool = false
var isReadOnly: Bool = false
var objectTarget: QuickSwitcherObjectTarget?
var target: QuickSwitcherTarget?

static func tableItemId(name: String, isView: Bool) -> String {
"table_\(name)_\(isView ? "VIEW" : "TABLE")"
Expand Down
56 changes: 56 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -24338,6 +24338,34 @@
}
}
},
"Could Not Open Query" : {
"localizations" : {
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sorgu Açılamadı"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Không thể mở truy vấn"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "无法打开查询"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "無法開啟查詢"
}
}
}
},
"Could Not Open Sample" : {
"localizations" : {
"tr" : {
Expand Down Expand Up @@ -90794,6 +90822,34 @@
}
}
},
"The target connection is no longer open." : {
"localizations" : {
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hedef bağlantı artık açık değil."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kết nối đích không còn mở."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "目标连接已不再打开。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標連線已不再開啟。"
}
}
}
},
"The target database may be in a partial state. Review the database and clean up as needed." : {
"comment" : "A message that appears when restoring a database and the user is advised to review and clean up the database.",
"isCommentAutoGenerated" : true,
Expand Down
Loading
Loading