Skip to content

Commit 8dbf970

Browse files
authored
fix(mssql, oracle): enable schema switching from Cmd+K quick switcher (#1050)
* fix(mssql, oracle): enable schema switching from Cmd+K quick switcher * test(plugin): cover MSSQL and Oracle schema-switching capability flag Also pin plugin-level supportsSchemaSwitching = true for MSSQL and Oracle so plugin metadata, registry default, and Quick Switcher allowlist all agree.
1 parent ac157ff commit 8dbf970

7 files changed

Lines changed: 139 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
### Fixed
1919

20+
- SQL Server and Oracle connections silently ignored schema selection from the Cmd+K Quick Switcher. Picking a schema like `rpt` (or any non-default) appeared in the search list but did nothing on selection. Three things were misaligned: `QuickSwitcherViewModel` listed both engines in a hardcoded schema-fetch allowlist (so schemas appeared as searchable items), but `PluginMetadataRegistry` had `supportsSchemaSwitching: false` for both, and `MainContentCoordinator+Navigation.switchSchema` early-returned silently on that flag with no log and no user feedback. The MSSQL and Oracle plugin drivers themselves already implemented `switchSchema(to:)` correctly (MSSQL updates `_currentSchema` for table-listing filters; Oracle runs `ALTER SESSION SET CURRENT_SCHEMA`), so flipping the registry flag is sufficient. `supportsSchemaSwitching` is now `true` for both, post-connect actions include `selectSchemaFromLastSession` so a chosen schema persists across reconnects, the `QuickSwitcherViewModel` allowlist is replaced with `PluginManager.shared.supportsSchemaSwitching(for:)` so future engines auto-pick up the right behavior, and the early-return in `switchSchema` now logs and surfaces a `Schema Switching Not Supported` alert instead of failing silently. Reported in r/macapps feedback.
2021
- iOS: app crashed with `EXC_BREAKPOINT` "Not enough bits to represent the passed value" when opening some MySQL tables (TestFlight report on a 100k-record table). `MySQLActor.execute` did `Int(mysql_affected_rows(mysql))`, but libmariadb is documented to return `~(my_ulonglong)0` (= `UInt64.max`) as an error sentinel ("for a SELECT, mysql_affected_rows() was called prior to mysql_store_result()") and the unchecked Int conversion trapped on the sentinel. The same shape applied to per-cell `mysql_fetch_lengths` values, which on arm64 are `unsigned long` (`UInt64`); a length above `Int.max` would trap rather than fail the read recoverably. Both paths now use `Int(clamping:)` and the affected-rows sites explicitly map the `~0` sentinel to `0`. Same hardening applied to the macOS MySQL plugin's two cell-length conversion sites in `MariaDBPluginConnection.swift` (default-fetch and streaming) which had identical exposure but no reported crash.
2122
- iOS: connections to `.local` (Bonjour) hostnames and other local-network addresses (10.x, 192.168.x, 172.16-31.x, 169.254.x, IPv6 ULA / link-local) timed out silently. The bundle was missing `NSLocalNetworkUsageDescription` and `NSBonjourServices`, so iOS never prompted the user for Local Network access and quietly dropped every outbound `connect()` to a local-network address. Most visible variant: SSH Tunnel set to `Some-MacBook.local`, error surfaced as "MySQL connection failed: Lost connection to server at 'handshake: reading initial communication packet', system error: 60" (errno 60 = `ETIMEDOUT`). Both Info.plist keys are now declared (purpose string explains database/SSH access; Bonjour types `_ssh._tcp`, `_mysql._tcp`, `_postgresql._tcp`, `_redis._tcp`). A new `LocalNetworkPermission` actor starts an `NWBrowser` for `_ssh._tcp` the first time a connection targets a local-network host (the documented Apple pattern from the DTS "Local Network Privacy FAQ" since a bare `connect()` does not always trigger the consent prompt for `getaddrinfo`-based connections), watches the `NWBrowser.State` stream for `.ready` (granted) or `.waiting`/`.failed` (unavailable), and caches the resolution per process. On denial the gate throws `LocalNetworkPermissionError.unavailable` immediately on every subsequent attempt instead of waiting for the 10-second TCP timeout, so the error surfaces in under a second. Concurrent first-time gate calls share one in-flight resolution `Task` so a parallel SSH + DB connect does not double-prompt. Wired through `SSHTunnelFactory.create()` and `MySQLDriver` / `PostgreSQLDriver` / `RedisDriver` `connect()` (loopback-only and non-local hosts no-op the gate). `ErrorClassifier` matches the typed `LocalNetworkPermissionError` directly and falls back to detecting `ETIMEDOUT` on SSH-enabled or local-network connections; both paths show "Open Settings > Privacy & Security > Local Network and turn TablePro on, then try again." instead of the previous generic "server is not responding" or misleading SSH-handshake copy.
2223
- Data grid column headers now use the same 4pt horizontal inset as result cells, including the right-aligned `#` row-number header.

Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
2424

2525
// MARK: - UI/Capability Metadata
2626

27-
static let postConnectActions: [PostConnectAction] = [.selectDatabaseFromLastSession]
27+
static let supportsSchemaSwitching = true
28+
static let postConnectActions: [PostConnectAction] = [.selectDatabaseFromLastSession, .selectSchemaFromLastSession]
2829
static let brandColorHex = "#E34517"
2930
static let systemDatabaseNames: [String] = ["master", "tempdb", "model", "msdb"]
3031
static let defaultSchemaName = "dbo"

Plugins/OracleDriverPlugin/OraclePlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin, PluginDiagnost
2626
static let isDownloadable = true
2727
static let pathFieldRole: PathFieldRole = .serviceName
2828
static let supportsForeignKeyDisable = false
29+
static let supportsSchemaSwitching = true
30+
static let postConnectActions: [PostConnectAction] = [.selectSchemaFromLastSession]
2931
static let brandColorHex = "#C3160B"
3032
static let systemDatabaseNames: [String] = ["SYS", "SYSTEM", "OUTLN", "DBSNMP", "APPQOSSYS", "WMSYS", "XDB"]
3133
static let databaseGroupingStrategy: GroupingStrategy = .bySchema

TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -647,13 +647,13 @@ extension PluginMetadataRegistry {
647647
isDownloadable: true, primaryUrlScheme: "sqlserver", parameterStyle: .questionMark,
648648
navigationModel: .standard, explainVariants: [], pathFieldRole: .database,
649649
supportsHealthMonitor: true, urlSchemes: ["sqlserver", "mssql"],
650-
postConnectActions: [.selectDatabaseFromLastSession],
650+
postConnectActions: [.selectDatabaseFromLastSession, .selectSchemaFromLastSession],
651651
brandColorHex: "#E34517",
652652
queryLanguageName: "SQL", editorLanguage: .sql,
653653
connectionMode: .network, supportsDatabaseSwitching: true,
654654
supportsColumnReorder: false,
655655
capabilities: PluginMetadataSnapshot.CapabilityFlags(
656-
supportsSchemaSwitching: false,
656+
supportsSchemaSwitching: true,
657657
supportsImport: true,
658658
supportsExport: true,
659659
supportsSSH: true,
@@ -698,13 +698,14 @@ extension PluginMetadataRegistry {
698698
requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true,
699699
isDownloadable: true, primaryUrlScheme: "oracle", parameterStyle: .questionMark,
700700
navigationModel: .standard, explainVariants: [], pathFieldRole: .serviceName,
701-
supportsHealthMonitor: true, urlSchemes: ["oracle"], postConnectActions: [],
701+
supportsHealthMonitor: true, urlSchemes: ["oracle"],
702+
postConnectActions: [.selectSchemaFromLastSession],
702703
brandColorHex: "#C3160B",
703704
queryLanguageName: "SQL", editorLanguage: .sql,
704705
connectionMode: .network, supportsDatabaseSwitching: true,
705706
supportsColumnReorder: false,
706707
capabilities: PluginMetadataSnapshot.CapabilityFlags(
707-
supportsSchemaSwitching: false,
708+
supportsSchemaSwitching: true,
708709
supportsImport: true,
709710
supportsExport: true,
710711
supportsSSH: true,

TablePro/ViewModels/QuickSwitcherViewModel.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ internal final class QuickSwitcherViewModel {
8686
Self.logger.warning("Failed to fetch databases for quick switcher: \(error.localizedDescription, privacy: .public)")
8787
}
8888

89-
// Schemas (only for databases that support them)
90-
let supportsSchemas = [DatabaseType.postgresql, .redshift, .oracle, .mssql]
91-
if supportsSchemas.contains(databaseType) {
89+
if PluginManager.shared.supportsSchemaSwitching(for: databaseType) {
9290
do {
9391
let schemas = try await driver.fetchSchemas()
9492
for schema in schemas {

TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,9 +428,21 @@ extension MainContentCoordinator {
428428
}
429429
}
430430

431-
/// Switch to a different PostgreSQL schema (used for URL-based schema selection)
432431
func switchSchema(to schema: String) async {
433-
guard PluginManager.shared.supportsSchemaSwitching(for: connection.type) else { return }
432+
guard PluginManager.shared.supportsSchemaSwitching(for: connection.type) else {
433+
navigationLogger.warning(
434+
"switchSchema(to: \(schema, privacy: .public)) ignored: \(connection.type.rawValue, privacy: .public) does not support schema switching"
435+
)
436+
AlertHelper.showErrorSheet(
437+
title: String(localized: "Schema Switching Not Supported"),
438+
message: String(
439+
format: String(localized: "%@ does not support switching schemas in TablePro."),
440+
connection.type.rawValue
441+
),
442+
window: contentWindow
443+
)
444+
return
445+
}
434446

435447
clearFilterState()
436448
let previousSchema = toolbarState.databaseName
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// PluginMetadataRegistrySchemaSwitchingTests.swift
3+
// TableProTests
4+
//
5+
6+
import Foundation
7+
@testable import TablePro
8+
import TableProPluginKit
9+
import Testing
10+
11+
@MainActor
12+
@Suite("PluginMetadataRegistry schema switching")
13+
struct PluginMetadataRegistrySchemaSwitchingTests {
14+
private func snapshot(forTypeId typeId: String) -> PluginMetadataSnapshot? {
15+
PluginMetadataRegistry.shared.snapshot(forTypeId: typeId)
16+
}
17+
18+
// MARK: - SQL Server
19+
20+
@Test("SQL Server supports schema switching")
21+
func sqlServerSupportsSchemaSwitching() {
22+
guard let snap = snapshot(forTypeId: "SQL Server") else {
23+
Issue.record("Registry default for SQL Server missing")
24+
return
25+
}
26+
#expect(snap.capabilities.supportsSchemaSwitching == true)
27+
}
28+
29+
@Test("SQL Server post-connect actions restore last schema")
30+
func sqlServerRestoresLastSchema() {
31+
guard let snap = snapshot(forTypeId: "SQL Server") else {
32+
Issue.record("Registry default for SQL Server missing")
33+
return
34+
}
35+
#expect(snap.postConnectActions.contains(.selectSchemaFromLastSession))
36+
}
37+
38+
@Test("SQL Server post-connect actions still restore last database")
39+
func sqlServerRestoresLastDatabase() {
40+
guard let snap = snapshot(forTypeId: "SQL Server") else {
41+
Issue.record("Registry default for SQL Server missing")
42+
return
43+
}
44+
#expect(snap.postConnectActions.contains(.selectDatabaseFromLastSession))
45+
}
46+
47+
// MARK: - Oracle
48+
49+
@Test("Oracle supports schema switching")
50+
func oracleSupportsSchemaSwitching() {
51+
guard let snap = snapshot(forTypeId: "Oracle") else {
52+
Issue.record("Registry default for Oracle missing")
53+
return
54+
}
55+
#expect(snap.capabilities.supportsSchemaSwitching == true)
56+
}
57+
58+
@Test("Oracle post-connect actions restore last schema")
59+
func oracleRestoresLastSchema() {
60+
guard let snap = snapshot(forTypeId: "Oracle") else {
61+
Issue.record("Registry default for Oracle missing")
62+
return
63+
}
64+
#expect(snap.postConnectActions.contains(.selectSchemaFromLastSession))
65+
}
66+
67+
// MARK: - PostgreSQL (regression for the working reference)
68+
69+
@Test("PostgreSQL supports schema switching")
70+
func postgreSQLSupportsSchemaSwitching() {
71+
guard let snap = snapshot(forTypeId: "PostgreSQL") else {
72+
Issue.record("Registry default for PostgreSQL missing")
73+
return
74+
}
75+
#expect(snap.capabilities.supportsSchemaSwitching == true)
76+
}
77+
78+
// MARK: - Negative cases (engines without schemas)
79+
80+
@Test("MySQL does not support schema switching")
81+
func mysqlDoesNotSupportSchemaSwitching() {
82+
guard let snap = snapshot(forTypeId: "MySQL") else {
83+
Issue.record("Registry default for MySQL missing")
84+
return
85+
}
86+
#expect(snap.capabilities.supportsSchemaSwitching == false)
87+
}
88+
89+
@Test("SQLite does not support schema switching")
90+
func sqliteDoesNotSupportSchemaSwitching() {
91+
guard let snap = snapshot(forTypeId: "SQLite") else {
92+
Issue.record("Registry default for SQLite missing")
93+
return
94+
}
95+
#expect(snap.capabilities.supportsSchemaSwitching == false)
96+
}
97+
98+
// MARK: - Cross-component consistency
99+
100+
@Test("Quick Switcher allowlist agrees with registry capability flag")
101+
func quickSwitcherAllowlistMatchesRegistry() {
102+
let typesThatShouldSupportSchemas = ["PostgreSQL", "Redshift", "Oracle", "SQL Server"]
103+
for typeId in typesThatShouldSupportSchemas {
104+
guard let snap = snapshot(forTypeId: typeId) else {
105+
Issue.record("Registry default for \(typeId) missing")
106+
continue
107+
}
108+
#expect(
109+
snap.capabilities.supportsSchemaSwitching == true,
110+
"\(typeId) is in the documented schema-aware engine set but registry has supportsSchemaSwitching = false"
111+
)
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)