Skip to content

Commit a149400

Browse files
committed
Merge branch 'main' into fix/memory-retention-after-tab-close
2 parents 38e46b8 + c50b52a commit a149400

31 files changed

Lines changed: 303 additions & 188 deletions

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- MongoDB Atlas connections failing to authenticate (#438)
13+
- MongoDB TLS certificate verification skipped for SRV connections
14+
- Active tab data no longer refreshes when switching back to the app window
15+
- Undo history preserved when switching between database tables
16+
- Health monitor now detects stuck queries beyond the configured timeout
17+
- SSH tunnel closure errors now logged instead of silently discarded
18+
- Schema/database restore errors during reconnect now logged
19+
20+
## [0.23.1] - 2026-03-24
21+
1022
### Added
1123

1224
- Test Connection button in SSH profile editor to validate SSH connectivity independently
@@ -1003,7 +1015,8 @@ TablePro is a native macOS database client built with SwiftUI and AppKit, design
10031015
- Custom SQL query templates
10041016
- Performance optimized for large datasets
10051017

1006-
[Unreleased]: https://github.com/datlechin/tablepro/compare/v0.23.0...HEAD
1018+
[Unreleased]: https://github.com/datlechin/tablepro/compare/v0.23.1...HEAD
1019+
[0.23.1]: https://github.com/datlechin/tablepro/compare/v0.23.0...v0.23.1
10071020
[0.23.0]: https://github.com/datlechin/tablepro/compare/v0.22.1...v0.23.0
10081021
[0.22.1]: https://github.com/datlechin/tablepro/compare/v0.22.0...v0.22.1
10091022
[0.22.0]: https://github.com/datlechin/tablepro/compare/v0.21.0...v0.22.0

Plugins/MongoDBDriverPlugin/MongoDBConnection.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ final class MongoDBConnection: @unchecked Sendable {
190190
let effectiveAuthSource: String
191191
if let source = authSource, !source.isEmpty {
192192
effectiveAuthSource = source
193+
} else if useSrv {
194+
effectiveAuthSource = "admin"
193195
} else if !database.isEmpty {
194196
effectiveAuthSource = database
195197
} else {
@@ -206,8 +208,7 @@ final class MongoDBConnection: @unchecked Sendable {
206208
let sslEnabled = ["Preferred", "Required", "Verify CA", "Verify Identity"].contains(sslMode)
207209
if sslEnabled {
208210
params.append("tls=true")
209-
let verifiesCert = sslMode == "Verify CA" || sslMode == "Verify Identity"
210-
if !verifiesCert {
211+
if sslMode == "Preferred" {
211212
params.append("tlsAllowInvalidCertificates=true")
212213
}
213214
if !sslCACertPath.isEmpty {

Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
3636
// MARK: - Connection Management
3737

3838
func connect() async throws {
39+
// Auto-enable SRV for Atlas hostnames (*.mongodb.net) even if the toggle wasn't set,
40+
// since Atlas clusters only resolve via SRV records.
3941
let useSrv = config.additionalFields["mongoUseSrv"] == "true"
42+
|| config.host.hasSuffix(".mongodb.net")
4043
let authMechanism = config.additionalFields["mongoAuthMechanism"]
4144
let replicaSet = config.additionalFields["mongoReplicaSet"]
4245

@@ -54,7 +57,9 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
5457
user: config.username,
5558
password: config.password,
5659
database: currentDb,
57-
sslMode: config.additionalFields["sslMode"] ?? "Disabled",
60+
sslMode: useSrv && (config.additionalFields["sslMode"] ?? "Disabled") == "Disabled"
61+
? "Required"
62+
: config.additionalFields["sslMode"] ?? "Disabled",
5863
sslCACertPath: config.additionalFields["sslCACertPath"] ?? "",
5964
sslClientCertPath: config.additionalFields["sslClientCertPath"] ?? "",
6065
authSource: config.additionalFields["mongoAuthSource"],

TablePro.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,7 +1920,7 @@
19201920
CODE_SIGN_IDENTITY = "Apple Development";
19211921
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
19221922
CODE_SIGN_STYLE = Automatic;
1923-
CURRENT_PROJECT_VERSION = 44;
1923+
CURRENT_PROJECT_VERSION = 45;
19241924
DEAD_CODE_STRIPPING = YES;
19251925
DEVELOPMENT_TEAM = D7HJ5TFYCU;
19261926
ENABLE_APP_SANDBOX = NO;
@@ -1945,7 +1945,7 @@
19451945
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
19461946
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
19471947
MACOSX_DEPLOYMENT_TARGET = 14.0;
1948-
MARKETING_VERSION = 0.23.0;
1948+
MARKETING_VERSION = 0.23.1;
19491949
OTHER_LDFLAGS = (
19501950
"-Wl,-w",
19511951
"-force_load",
@@ -1992,7 +1992,7 @@
19921992
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
19931993
CODE_SIGN_STYLE = Automatic;
19941994
COPY_PHASE_STRIP = YES;
1995-
CURRENT_PROJECT_VERSION = 44;
1995+
CURRENT_PROJECT_VERSION = 45;
19961996
DEAD_CODE_STRIPPING = YES;
19971997
DEPLOYMENT_POSTPROCESSING = YES;
19981998
DEVELOPMENT_TEAM = D7HJ5TFYCU;
@@ -2018,7 +2018,7 @@
20182018
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
20192019
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
20202020
MACOSX_DEPLOYMENT_TARGET = 14.0;
2021-
MARKETING_VERSION = 0.23.0;
2021+
MARKETING_VERSION = 0.23.1;
20222022
OTHER_LDFLAGS = (
20232023
"-Wl,-w",
20242024
"-force_load",

TablePro/AppDelegate+WindowConfig.swift

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,22 @@ extension AppDelegate {
100100

101101
// MARK: - Window Identification
102102

103+
private enum WindowId {
104+
static let main = "main"
105+
static let welcome = "welcome"
106+
static let connectionForm = "connection-form"
107+
}
108+
103109
func isMainWindow(_ window: NSWindow) -> Bool {
104-
guard let identifier = window.identifier?.rawValue else { return false }
105-
return identifier.contains("main")
110+
window.identifier?.rawValue == WindowId.main
106111
}
107112

108113
func isWelcomeWindow(_ window: NSWindow) -> Bool {
109-
window.identifier?.rawValue == "welcome" ||
110-
window.title.lowercased().contains("welcome")
114+
window.identifier?.rawValue == WindowId.welcome
111115
}
112116

113117
private func isConnectionFormWindow(_ window: NSWindow) -> Bool {
114-
window.identifier?.rawValue.contains("connection-form") == true
118+
window.identifier?.rawValue == WindowId.connectionForm
115119
}
116120

117121
// MARK: - Welcome Window
@@ -259,10 +263,7 @@ extension AppDelegate {
259263

260264
if remainingMainWindows == 0 {
261265
NotificationCenter.default.post(name: .mainWindowWillClose, object: nil)
262-
263-
DispatchQueue.main.async {
264-
self.openWelcomeWindow()
265-
}
266+
openWelcomeWindow()
266267
}
267268
}
268269
}
@@ -273,13 +274,9 @@ extension AppDelegate {
273274

274275
if isWelcomeWindow(window),
275276
window.occlusionState.contains(.visible),
276-
NSApp.windows.contains(where: { isMainWindow($0) && $0.isVisible }) {
277-
DispatchQueue.main.async { [weak self] in
278-
guard let self else { return }
279-
if self.isWelcomeWindow(window), window.isVisible {
280-
window.close()
281-
}
282-
}
277+
NSApp.windows.contains(where: { isMainWindow($0) && $0.isVisible }),
278+
window.isVisible {
279+
window.close()
283280
}
284281
}
285282

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ final class DataChangeManager {
125125

126126
// MARK: - Configuration
127127

128-
/// Clear all changes (called after successful save)
128+
/// Clear all tracked changes, preserving undo/redo history.
129+
/// Use when changes are invalidated but undo context may still be relevant.
129130
func clearChanges() {
130131
changes.removeAll()
131132
changeIndex.removeAll()
@@ -134,11 +135,18 @@ final class DataChangeManager {
134135
modifiedCells.removeAll()
135136
insertedRowData.removeAll()
136137
changedRowIndices.removeAll()
137-
undoManager.clearAll()
138138
hasChanges = false
139139
reloadVersion += 1
140140
}
141141

142+
/// Clear all tracked changes AND undo/redo history.
143+
/// Use after successful save, explicit discard, or new query execution
144+
/// where undo context is no longer meaningful.
145+
func clearChangesAndUndoHistory() {
146+
clearChanges()
147+
undoManager.clearAll()
148+
}
149+
142150
/// Atomically configure the manager for a new table
143151
func configureForTable(
144152
tableName: String,

TablePro/Core/Database/ConnectionHealthMonitor.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ actor ConnectionHealthMonitor {
3535
// MARK: - Configuration
3636

3737
private static let pingInterval: TimeInterval = 30.0
38-
private static let initialBackoffDelays: [TimeInterval] = [2.0, 4.0, 8.0]
3938
private static let maxBackoffDelay: TimeInterval = 120.0
4039

4140
// MARK: - Dependencies
@@ -227,15 +226,7 @@ actor ConnectionHealthMonitor {
227226
/// Uses the initial delay table for the first few attempts, then doubles
228227
/// the previous delay for subsequent attempts, capped at `maxBackoffDelay`.
229228
private func backoffDelay(for attempt: Int) -> TimeInterval {
230-
let delays = Self.initialBackoffDelays
231-
if attempt <= delays.count {
232-
return delays[attempt - 1]
233-
}
234-
// Exponential: last seed delay * 2^(attempt - seedCount)
235-
let lastSeed = delays[delays.count - 1]
236-
let exponent = attempt - delays.count
237-
let computed = lastSeed * pow(2.0, Double(exponent))
238-
return min(computed, Self.maxBackoffDelay)
229+
ExponentialBackoff.delay(for: attempt, maxDelay: Self.maxBackoffDelay)
239230
}
240231

241232
// MARK: - State Transitions

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,15 @@ extension DatabaseDriver {
315315
/// Factory for creating database drivers via plugin lookup
316316
@MainActor
317317
enum DatabaseDriverFactory {
318+
private static let logger = Logger(subsystem: "com.TablePro", category: "DatabaseDriverFactory")
319+
318320
static func createDriver(for connection: DatabaseConnection) throws -> DatabaseDriver {
319321
let pluginId = connection.type.pluginTypeId
320322
// If the plugin isn't registered yet and background loading hasn't finished,
321323
// fall back to synchronous loading for this critical code path.
322324
if PluginManager.shared.driverPlugins[pluginId] == nil,
323325
!PluginManager.shared.hasFinishedInitialLoad {
326+
logger.warning("Plugin '\(pluginId)' not loaded yet — performing synchronous load")
324327
PluginManager.shared.loadPendingPlugins()
325328
}
326329
guard let plugin = PluginManager.shared.driverPlugins[pluginId] else {

0 commit comments

Comments
 (0)