diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6a3a304..59cf74653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the query result row cap returning a single row when it was set to unlimited. (#1884) - Fixed SSH tunnels that could accept a database connection and then go quiet instead of forwarding it or failing, which showed up as MySQL and MariaDB connections timing out while reading the server greeting. A tunnel that cannot open its forwarding channel now gives up after 10 seconds, closes the connection, and logs the reason, instead of leaving the driver to wait out its own timeout with no explanation. (#1883) - Fixed connections being reset when a single database session opened several at once through one SSH tunnel, which could happen while browsing schemas. (#1883) +- Fixed the connection form's SSH tunnel Socket Path hint always showing the PostgreSQL socket path. It now shows the right default for the database type, such as `/var/run/mysqld/mysqld.sock` for MySQL and MariaDB. (#1902) - Per-column display formats (Display As) are now kept per table, so two tables with the same name in different databases or schemas no longer share formatting. - Reopening a table now restores a saved filter's AND/OR match mode, instead of always resetting it to AND. diff --git a/TablePro/Core/Plugins/PluginManager+Registration.swift b/TablePro/Core/Plugins/PluginManager+Registration.swift index d4dda687d..3ac52eac9 100644 --- a/TablePro/Core/Plugins/PluginManager+Registration.swift +++ b/TablePro/Core/Plugins/PluginManager+Registration.swift @@ -397,6 +397,11 @@ extension PluginManager { .schema.containerEntityName ?? "Database" } + func defaultUnixSocketPath(for databaseType: DatabaseType) -> String? { + PluginMetadataRegistry.shared.snapshot(forTypeId: databaseType.pluginTypeId)? + .connection.defaultUnixSocketPath + } + func containerEntityNamePlural(for databaseType: DatabaseType) -> String { containerEntityName(for: databaseType) + "s" } diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift index 244e7ee0f..0a6eff2f0 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift @@ -152,7 +152,8 @@ extension PluginMetadataRegistry { ) ], category: .keyValue, - tagline: String(localized: "In-memory data store and cache") + tagline: String(localized: "In-memory data store and cache"), + defaultUnixSocketPath: "/var/run/redis/redis.sock" ) )), ("SQL Server", PluginMetadataSnapshot( diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry.swift b/TablePro/Core/Plugins/PluginMetadataRegistry.swift index afafbc51a..bff747d0b 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry.swift @@ -171,17 +171,20 @@ struct PluginMetadataSnapshot: Sendable { let category: DatabaseCategory let tagline: String let hidesBuiltInPassword: Bool + let defaultUnixSocketPath: String? init( additionalConnectionFields: [ConnectionField] = [], category: DatabaseCategory = .other, tagline: String = "", - hidesBuiltInPassword: Bool = false + hidesBuiltInPassword: Bool = false, + defaultUnixSocketPath: String? = nil ) { self.additionalConnectionFields = additionalConnectionFields self.category = category self.tagline = tagline self.hidesBuiltInPassword = hidesBuiltInPassword + self.defaultUnixSocketPath = defaultUnixSocketPath } static let defaults = ConnectionConfig() @@ -543,7 +546,8 @@ final class PluginMetadataRegistry: @unchecked Sendable { connection: PluginMetadataSnapshot.ConnectionConfig( additionalConnectionFields: awsIAMFields, category: .relational, - tagline: String(localized: "Most popular open-source SQL database") + tagline: String(localized: "Most popular open-source SQL database"), + defaultUnixSocketPath: "/var/run/mysqld/mysqld.sock" ) )), ("MariaDB", PluginMetadataSnapshot( @@ -594,7 +598,8 @@ final class PluginMetadataRegistry: @unchecked Sendable { connection: PluginMetadataSnapshot.ConnectionConfig( additionalConnectionFields: awsIAMFields, category: .relational, - tagline: String(localized: "Open-source fork of MySQL") + tagline: String(localized: "Open-source fork of MySQL"), + defaultUnixSocketPath: "/var/run/mysqld/mysqld.sock" ) )), ("PostgreSQL", PluginMetadataSnapshot( @@ -646,7 +651,8 @@ final class PluginMetadataRegistry: @unchecked Sendable { connection: PluginMetadataSnapshot.ConnectionConfig( additionalConnectionFields: [pgpassField, connectionOptionsField] + awsIAMFields, category: .relational, - tagline: String(localized: "Advanced object-relational SQL") + tagline: String(localized: "Advanced object-relational SQL"), + defaultUnixSocketPath: "/var/run/postgresql/.s.PGSQL.5432" ) )), ("Redshift", PluginMetadataSnapshot( @@ -1023,7 +1029,8 @@ final class PluginMetadataRegistry: @unchecked Sendable { ?? Self.fallbackCategory(forTypeId: driverType.databaseTypeId), tagline: existingSnapshot?.connection.tagline ?? Self.fallbackTagline(forTypeId: driverType.databaseTypeId), - hidesBuiltInPassword: existingSnapshot?.connection.hidesBuiltInPassword ?? false + hidesBuiltInPassword: existingSnapshot?.connection.hidesBuiltInPassword ?? false, + defaultUnixSocketPath: existingSnapshot?.connection.defaultUnixSocketPath ) ) } diff --git a/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift b/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift index b90e967ee..9bf9a86f5 100644 --- a/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift +++ b/TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift @@ -174,7 +174,7 @@ struct GeneralPaneView: View { TextField( String(localized: "Socket Path"), text: $coordinator.network.sshForwardUnixSocketPath, - prompt: Text(verbatim: "/var/run/postgresql/.s.PGSQL.5432") + prompt: Text(verbatim: coordinator.network.socketPathPrompt) ) switch coordinator.network.socketPathIssue { case .notAbsolute: diff --git a/TablePro/Views/ConnectionForm/ViewModels/NetworkPaneViewModel.swift b/TablePro/Views/ConnectionForm/ViewModels/NetworkPaneViewModel.swift index e6993ed0c..00b59c446 100644 --- a/TablePro/Views/ConnectionForm/ViewModels/NetworkPaneViewModel.swift +++ b/TablePro/Views/ConnectionForm/ViewModels/NetworkPaneViewModel.swift @@ -44,6 +44,10 @@ final class NetworkPaneViewModel { return port == 0 ? "" : String(port) } + var socketPathPrompt: String { + PluginManager.shared.defaultUnixSocketPath(for: type) ?? "/path/to/database.sock" + } + var resolvedHost: String { host.trimmingCharacters(in: .whitespaces).isEmpty ? "localhost" : host } diff --git a/TableProTests/Core/Plugins/SocketPathPlaceholderTests.swift b/TableProTests/Core/Plugins/SocketPathPlaceholderTests.swift new file mode 100644 index 000000000..3c9da2c0f --- /dev/null +++ b/TableProTests/Core/Plugins/SocketPathPlaceholderTests.swift @@ -0,0 +1,41 @@ +// +// SocketPathPlaceholderTests.swift +// TableProTests +// + +import Foundation +@testable import TablePro +import TableProPluginKit +import Testing + +@MainActor +@Suite("Default Unix socket path per database type") +struct SocketPathPlaceholderTests { + @Test("MySQL and MariaDB use the mysqld socket") + func mysqlFamilyUsesMysqldSocket() { + #expect(PluginManager.shared.defaultUnixSocketPath(for: .mysql) == "/var/run/mysqld/mysqld.sock") + #expect(PluginManager.shared.defaultUnixSocketPath(for: .mariadb) == "/var/run/mysqld/mysqld.sock") + } + + @Test("PostgreSQL uses the PGSQL socket") + func postgresqlUsesPgsqlSocket() { + #expect(PluginManager.shared.defaultUnixSocketPath(for: .postgresql) == "/var/run/postgresql/.s.PGSQL.5432") + } + + @Test("Redis uses the redis socket") + func redisUsesRedisSocket() { + #expect(PluginManager.shared.defaultUnixSocketPath(for: .redis) == "/var/run/redis/redis.sock") + } + + @Test("Types without a socket convention have no default") + func typesWithoutSocketHaveNoDefault() { + #expect(PluginManager.shared.defaultUnixSocketPath(for: .sqlite) == nil) + #expect(PluginManager.shared.defaultUnixSocketPath(for: .clickhouse) == nil) + } + + @Test("Unknown type has no default") + func unknownTypeHasNoDefault() { + let unknown = DatabaseType(rawValue: "FuturePlugin") + #expect(PluginManager.shared.defaultUnixSocketPath(for: unknown) == nil) + } +} diff --git a/TableProTests/Views/ConnectionForm/NetworkPaneSocketForwardTests.swift b/TableProTests/Views/ConnectionForm/NetworkPaneSocketForwardTests.swift index 8c2c11a0d..a0d896a21 100644 --- a/TableProTests/Views/ConnectionForm/NetworkPaneSocketForwardTests.swift +++ b/TableProTests/Views/ConnectionForm/NetworkPaneSocketForwardTests.swift @@ -70,4 +70,23 @@ struct NetworkPaneSocketForwardTests { #expect(fields[DatabaseConnection.sshForwardUnixSocketPathKey] == nil) #expect(viewModel.forwardsToUnixSocket == false) } + + @Test("The socket path prompt follows the database type") + func socketPromptFollowsType() { + let viewModel = NetworkPaneViewModel() + + viewModel.type = .mysql + #expect(viewModel.socketPathPrompt == "/var/run/mysqld/mysqld.sock") + + viewModel.type = .postgresql + #expect(viewModel.socketPathPrompt == "/var/run/postgresql/.s.PGSQL.5432") + } + + @Test("A type without a socket convention falls back to a generic prompt") + func socketPromptFallsBackForSocketlessType() { + let viewModel = NetworkPaneViewModel() + viewModel.type = .clickhouse + + #expect(viewModel.socketPathPrompt == "/path/to/database.sock") + } }