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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- The `postgres` database shows in the database list again. It was marked as a system database, which hid it from the sidebar, Cmd+K, the database filter, and the Backup and Restore Dump pickers. PostgreSQL creates it for users and applications, so nothing about it is internal. CockroachDB's `defaultdb` and Redshift's `dev` were hidden the same way and now show too. (#1967)
- The database a connection is using always shows in the sidebar and the database switchers, even when it is a system database or the database filter excludes it. (#1967)
- The license activation sheet now opens when you click **Activate License**. It was built and then failed to appear, and once that happened further clicks did nothing at all.
- File > Import from Other App..., Open Project Folder..., Import Connections... and Export Connections... now work when no welcome window is open. They used to do nothing.
- The tooltip on the welcome screen's **+** button shows the shortcut you actually have bound for New Connection instead of always claiming ⌘N.
Expand Down
3 changes: 1 addition & 2 deletions Plugins/PostgreSQLDriverPlugin/CockroachPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,11 @@ final class CockroachPluginDriver: LibPQBackedDriver, @unchecked Sendable {
.flatMap { $0.rows.first?.first?.asText }
.flatMap { Int($0) }

let systemDatabases = ["postgres", "system", "defaultdb"]
return PluginDatabaseMetadata(
name: database,
tableCount: tableCount,
sizeBytes: nil,
isSystemDatabase: systemDatabases.contains(database)
isSystemDatabase: PostgreSQLSystemDatabases.cockroachDB.contains(database)
)
}

Expand Down
2 changes: 1 addition & 1 deletion Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {

static let urlSchemes: [String] = ["postgresql", "postgres"]
static let brandColorHex = "#336791"
static let systemDatabaseNames: [String] = ["postgres", "template0", "template1"]
static let systemDatabaseNames: [String] = PostgreSQLSystemDatabases.postgreSQL
static let supportsSchemaSwitching = true
static let postConnectActions: [PostConnectAction] = [.selectSchemaFromLastSession]
static let explainVariants: [ExplainVariant] = [
Expand Down
13 changes: 6 additions & 7 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -667,19 +667,15 @@ class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
let tableCount = Int(row?[0].asText ?? "0") ?? 0
let sizeBytes = Int64(row?[1].asText ?? "0") ?? 0

let systemDatabases = ["postgres", "template0", "template1"]
let isSystem = systemDatabases.contains(database)

return PluginDatabaseMetadata(
name: database,
tableCount: tableCount,
sizeBytes: sizeBytes,
isSystemDatabase: isSystem
isSystemDatabase: PostgreSQLSystemDatabases.postgreSQL.contains(database)
)
}

func fetchAllDatabaseMetadata() async throws -> [PluginDatabaseMetadata] {
let systemDatabases = ["postgres", "template0", "template1"]
let query = """
SELECT d.datname, pg_database_size(d.datname)
FROM pg_database d
Expand All @@ -690,8 +686,11 @@ class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
return result.rows.compactMap { row -> PluginDatabaseMetadata? in
guard let dbName = row[0].asText else { return nil }
let sizeBytes = Int64(row[1].asText ?? "0") ?? 0
let isSystem = systemDatabases.contains(dbName)
return PluginDatabaseMetadata(name: dbName, sizeBytes: sizeBytes, isSystemDatabase: isSystem)
return PluginDatabaseMetadata(
name: dbName,
sizeBytes: sizeBytes,
isSystemDatabase: PostgreSQLSystemDatabases.postgreSQL.contains(dbName)
)
}
}

Expand Down
12 changes: 12 additions & 0 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLSystemDatabases.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// PostgreSQLSystemDatabases.swift
// PostgreSQLDriverPlugin
//

import Foundation

enum PostgreSQLSystemDatabases {
static let postgreSQL: [String] = []
static let redshift: [String] = ["padb_harvest"]
static let cockroachDB: [String] = ["system"]
}
9 changes: 2 additions & 7 deletions Plugins/PostgreSQLDriverPlugin/RedshiftPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,19 +389,15 @@ final class RedshiftPluginDriver: LibPQBackedDriver, @unchecked Sendable {
let sizeMb = Int64(sizeRes.rows.first?[0].asText ?? "0") ?? 0
let sizeBytes = sizeMb * 1_024 * 1_024

let systemDatabases = ["dev", "padb_harvest"]
let isSystem = systemDatabases.contains(database)

return PluginDatabaseMetadata(
name: database,
tableCount: tableCount,
sizeBytes: sizeBytes,
isSystemDatabase: isSystem
isSystemDatabase: PostgreSQLSystemDatabases.redshift.contains(database)
)
}

func fetchAllDatabaseMetadata() async throws -> [PluginDatabaseMetadata] {
let systemDatabases = ["dev", "padb_harvest"]
let dbResult = try await execute(
query: "SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname"
)
Expand All @@ -423,13 +419,12 @@ final class RedshiftPluginDriver: LibPQBackedDriver, @unchecked Sendable {
}

return dbNames.map { dbName in
let isSystem = systemDatabases.contains(dbName)
let info = metadataByName[dbName]
return PluginDatabaseMetadata(
name: dbName,
tableCount: info?.tableCount,
sizeBytes: info.map { $0.sizeMb * 1_024 * 1_024 },
isSystemDatabase: isSystem
isSystemDatabase: PostgreSQLSystemDatabases.redshift.contains(dbName)
)
}
}
Expand Down
8 changes: 4 additions & 4 deletions TablePro/Core/Plugins/PluginMetadataRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ final class PluginMetadataRegistry: @unchecked Sendable {
containerEntityName: "Database",
defaultPrimaryKeyColumn: nil,
immutableColumns: [],
systemDatabaseNames: ["postgres", "template0", "template1"],
systemDatabaseNames: [],
systemSchemaNames: [],
fileExtensions: [],
databaseGroupingStrategy: .bySchema,
Expand Down Expand Up @@ -691,7 +691,7 @@ final class PluginMetadataRegistry: @unchecked Sendable {
containerEntityName: "Database",
defaultPrimaryKeyColumn: nil,
immutableColumns: [],
systemDatabaseNames: ["postgres", "template0", "template1"],
systemDatabaseNames: ["padb_harvest"],
systemSchemaNames: [],
fileExtensions: [],
databaseGroupingStrategy: .bySchema,
Expand Down Expand Up @@ -752,7 +752,7 @@ final class PluginMetadataRegistry: @unchecked Sendable {
containerEntityName: "Database",
defaultPrimaryKeyColumn: nil,
immutableColumns: [],
systemDatabaseNames: ["postgres", "system", "defaultdb"],
systemDatabaseNames: ["system"],
systemSchemaNames: [],
fileExtensions: [],
databaseGroupingStrategy: .bySchema,
Expand Down Expand Up @@ -806,7 +806,7 @@ final class PluginMetadataRegistry: @unchecked Sendable {
containerEntityName: "Database",
defaultPrimaryKeyColumn: nil,
immutableColumns: [],
systemDatabaseNames: ["postgres", "template0", "template1"],
systemDatabaseNames: [],
systemSchemaNames: [],
fileExtensions: [],
databaseGroupingStrategy: .bySchema,
Expand Down
15 changes: 11 additions & 4 deletions TablePro/Core/Services/Query/DatabaseTreeVisibility.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import Foundation

enum DatabaseTreeVisibility {
static func visible(databases: [DatabaseMetadata], selected: Set<String>) -> [DatabaseMetadata] {
let nonSystem = databases.filter { !$0.isSystemDatabase }
guard !selected.isEmpty else { return nonSystem }
return nonSystem.filter { selected.contains($0.name) }
static func visible(
databases: [DatabaseMetadata],
selected: Set<String>,
activeDatabase: String?
) -> [DatabaseMetadata] {
let active = activeDatabase.flatMap { $0.isEmpty ? nil : $0 }
return databases.filter { database in
if database.name == active { return true }
guard !database.isSystemDatabase else { return false }
return selected.isEmpty || selected.contains(database.name)
}
}

static func isFiltering(selected: Set<String>) -> Bool {
Expand Down
8 changes: 6 additions & 2 deletions TablePro/ViewModels/DatabaseSwitcherViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ final class DatabaseSwitcherViewModel {
private let sidebarState: SharedSidebarState?

private var treeVisibleDatabases: [DatabaseMetadata] {
guard switchTarget == .database, let sidebarState else { return databases }
return DatabaseTreeVisibility.visible(databases: databases, selected: sidebarState.databaseFilterSelected)
guard switchTarget == .database else { return databases }
return DatabaseTreeVisibility.visible(
databases: databases,
selected: sidebarState?.databaseFilterSelected ?? [],
activeDatabase: currentDatabase
)
}

var filteredDatabases: [DatabaseMetadata] {
Expand Down
7 changes: 5 additions & 2 deletions TablePro/ViewModels/QuickSwitcherViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,14 @@ internal final class QuickSwitcherViewModel {

let switchTarget = services.pluginManager.containerSwitchTarget(for: databaseType)
let databaseFilter = SharedSidebarState.forConnection(connectionId).databaseFilterSelected
let activeDatabase = services.databaseManager.session(for: connectionId)
.map { services.databaseManager.activeDatabaseName(for: $0.connection) }
let visibleDatabaseNames = switchTarget == .database
? Set(
DatabaseTreeVisibility.visible(
databases: DatabaseTreeMetadataService.shared.databases(for: connectionId),
selected: databaseFilter
selected: databaseFilter,
activeDatabase: activeDatabase
).map(\.name)
)
: []
Expand All @@ -146,7 +149,7 @@ internal final class QuickSwitcherViewModel {
if switchTarget == .database {
if !visibleDatabaseNames.isEmpty {
if !visibleDatabaseNames.contains(db) { continue }
} else if !databaseFilter.isEmpty, !databaseFilter.contains(db) {
} else if !databaseFilter.isEmpty, db != activeDatabase, !databaseFilter.contains(db) {
continue
}
}
Expand Down
12 changes: 5 additions & 7 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,13 +300,11 @@ struct MainEditorContentView: View {
guard containerSwitchTarget == .database else { return [] }
let all = treeService.databases(for: connectionId)
let selected = SharedSidebarState.forConnection(connectionId).databaseFilterSelected
var visible = DatabaseTreeVisibility.visible(databases: all, selected: selected)
let bound = containerName(for: tab)
if !bound.isEmpty, !visible.contains(where: { $0.name == bound }),
let boundDatabase = all.first(where: { $0.name == bound }) {
visible.insert(boundDatabase, at: 0)
}
return visible
return DatabaseTreeVisibility.visible(
databases: all,
selected: selected,
activeDatabase: containerName(for: tab)
)
}

private var isContainerSwitchReadOnly: Bool {
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Sidebar/DatabaseTreeOutlineCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ final class DatabaseTreeOutlineCoordinator: NSObject {
}
let visible = DatabaseTreeVisibility.visible(
databases: service.databases(for: connectionId),
selected: sidebarState?.databaseFilterSelected ?? []
selected: sidebarState?.databaseFilterSelected ?? [],
activeDatabase: mainCoordinator?.activeDatabaseName ?? activeDatabase
)
let matched = searchText.isEmpty ? visible : visible.filter { databaseMatchesSearch($0) }
var seen = Set<String>()
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Views/Sidebar/DatabaseTreeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ struct DatabaseTreeView: View {
}

private var filteredDatabases: [DatabaseMetadata] {
DatabaseTreeVisibility.visible(databases: databases, selected: sidebarState.databaseFilterSelected)
DatabaseTreeVisibility.visible(
databases: databases,
selected: sidebarState.databaseFilterSelected,
activeDatabase: activeDatabase
)
}

private var isFilterHidingEverything: Bool {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// PluginMetadataRegistrySystemDatabaseTests.swift
// TableProTests
//
// The registry's curated defaults are the pre-plugin-load classification and
// must agree with what the PostgreSQL plugin reports once it registers.
// Redshift shipped a disagreement that flipped which database was hidden
// partway through loading the switcher (#1967).
//

import Foundation
@testable import TablePro
import TableProPluginKit
import Testing

@MainActor
@Suite("PluginMetadataRegistry system databases")
struct PluginMetadataRegistrySystemDatabaseTests {
private func systemDatabaseNames(forTypeId typeId: String) -> [String]? {
PluginMetadataRegistry.shared.snapshot(forTypeId: typeId)?.schema.systemDatabaseNames
}

@Test("PostgreSQL default matches the plugin")
func postgreSQLMatchesPlugin() {
#expect(systemDatabaseNames(forTypeId: "PostgreSQL") == PostgreSQLSystemDatabases.postgreSQL)
}

@Test("PGlite default matches the plugin")
func pgliteMatchesPlugin() {
#expect(systemDatabaseNames(forTypeId: "PGlite") == PostgreSQLSystemDatabases.postgreSQL)
}

@Test("Redshift default matches the plugin")
func redshiftMatchesPlugin() {
#expect(systemDatabaseNames(forTypeId: "Redshift") == PostgreSQLSystemDatabases.redshift)
}

@Test("CockroachDB default matches the plugin")
func cockroachMatchesPlugin() {
#expect(systemDatabaseNames(forTypeId: "CockroachDB") == PostgreSQLSystemDatabases.cockroachDB)
}

@Test("No PostgreSQL-family engine hides its default landing database")
func defaultLandingDatabasesStayVisible() {
let defaults = [
("PostgreSQL", "postgres"),
("PGlite", "postgres"),
("Redshift", "dev"),
("CockroachDB", "defaultdb"),
]
for (typeId, database) in defaults {
guard let names = systemDatabaseNames(forTypeId: typeId) else {
Issue.record("Registry default for \(typeId) missing")
continue
}
#expect(
!names.contains(database),
"\(typeId) marks its default database \(database) as system, which hides it from every database list"
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,68 @@ struct DatabaseTreeVisibilityTests {

@Test("Empty selection shows all non-system databases")
func emptyShowsAll() {
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: [])
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: [], activeDatabase: nil)
#expect(visible.map(\.name) == ["analytics", "billing", "legacy_2019"])
}

@Test("Non-empty selection shows only the selected non-system databases")
func selectionShowsSubset() {
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: ["billing", "legacy_2019"])
let visible = DatabaseTreeVisibility.visible(
databases: databases,
selected: ["billing", "legacy_2019"],
activeDatabase: nil
)
#expect(visible.map(\.name) == ["billing", "legacy_2019"])
}

@Test("System databases are never shown even when selected")
func systemNeverShown() {
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: ["mysql", "analytics"])
@Test("System databases are hidden even when selected")
func systemHiddenWhenSelected() {
let visible = DatabaseTreeVisibility.visible(
databases: databases,
selected: ["mysql", "analytics"],
activeDatabase: nil
)
#expect(visible.map(\.name) == ["analytics"])
}

@Test("Selecting a database that no longer exists yields an empty result")
func staleSelectionEmpty() {
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: ["dropped_db"])
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: ["dropped_db"], activeDatabase: nil)
#expect(visible.isEmpty)
}

@Test("The active database stays visible even when it is a system database")
func activeSystemDatabaseStaysVisible() {
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: [], activeDatabase: "mysql")
#expect(visible.map(\.name) == ["analytics", "billing", "legacy_2019", "mysql"])
}

@Test("The active database stays visible when the filter excludes it")
func activeDatabaseSurvivesFilter() {
let visible = DatabaseTreeVisibility.visible(
databases: databases,
selected: ["billing"],
activeDatabase: "analytics"
)
#expect(visible.map(\.name) == ["analytics", "billing"])
}

@Test("The active database keeps its position in the list")
func activeDatabaseKeepsPosition() {
let visible = DatabaseTreeVisibility.visible(
databases: databases,
selected: [],
activeDatabase: "information_schema"
)
#expect(visible.map(\.name) == ["analytics", "billing", "legacy_2019", "information_schema"])
}

@Test("An empty active database name is treated as absent")
func emptyActiveDatabaseIgnored() {
let visible = DatabaseTreeVisibility.visible(databases: databases, selected: [], activeDatabase: "")
#expect(visible.map(\.name) == ["analytics", "billing", "legacy_2019"])
}

@Test("isFiltering reflects whether a selection is active")
func isFiltering() {
#expect(DatabaseTreeVisibility.isFiltering(selected: []) == false)
Expand Down
Loading
Loading