Skip to content

Commit d335145

Browse files
authored
fix: support mongodb+srv:// URIs and speed up collection loading (#419) (#425)
* fix: support mongodb+srv:// URIs, bundle plugin, skip duplicates, and speed up collection loading (#419) * perf: reduce MongoDB collection load time with estimatedDocumentCount and smaller schema sample * revert: remove MongoDB plugin from built-in app bundle * docs: fix CHANGELOG entries to use Added/Changed instead of Fixed for unreleased features * fix: address PR review — serverVersion queue dispatch, explicitKeys filter, SSH param forwarding, stale field cleanup
1 parent 7fa94b9 commit d335145

File tree

10 files changed

+243
-26
lines changed

10 files changed

+243
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Keyboard focus navigation (Tab, Ctrl+J/K/N/P, arrow keys) for connection list, quick switcher, and database switcher
13+
- MongoDB `mongodb+srv://` URI support with SRV toggle, Auth Mechanism dropdown, and Replica Set field (#419)
14+
15+
### Changed
16+
17+
- MongoDB `authSource` defaults to database name per MongoDB URI spec instead of always "admin"
18+
- MongoDB collection loading uses `estimatedDocumentCount` and smaller schema sample for faster sidebar population
1319

1420
## [0.22.1] - 2026-03-22
1521

Plugins/MongoDBDriverPlugin/MongoDBConnection.swift

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ final class MongoDBConnection: @unchecked Sendable {
6262
private let authSource: String?
6363
private let readPreference: String?
6464
private let writeConcern: String?
65+
private let useSrv: Bool
66+
private let authMechanism: String?
67+
private let replicaSet: String?
68+
private let extraUriParams: [String: String]
6569

6670
private let stateLock = NSLock()
6771
private var _isConnected: Bool = false
@@ -114,7 +118,11 @@ final class MongoDBConnection: @unchecked Sendable {
114118
sslClientCertPath: String = "",
115119
authSource: String? = nil,
116120
readPreference: String? = nil,
117-
writeConcern: String? = nil
121+
writeConcern: String? = nil,
122+
useSrv: Bool = false,
123+
authMechanism: String? = nil,
124+
replicaSet: String? = nil,
125+
extraUriParams: [String: String] = [:]
118126
) {
119127
self.host = host
120128
self.port = port
@@ -127,6 +135,10 @@ final class MongoDBConnection: @unchecked Sendable {
127135
self.authSource = authSource
128136
self.readPreference = readPreference
129137
self.writeConcern = writeConcern
138+
self.useSrv = useSrv
139+
self.authMechanism = authMechanism
140+
self.replicaSet = replicaSet
141+
self.extraUriParams = extraUriParams
130142
}
131143

132144
deinit {
@@ -150,7 +162,8 @@ final class MongoDBConnection: @unchecked Sendable {
150162
// MARK: - URI Construction
151163

152164
private func buildUri() -> String {
153-
var uri = "mongodb://"
165+
let scheme = useSrv ? "mongodb+srv" : "mongodb"
166+
var uri = "\(scheme)://"
154167

155168
if !user.isEmpty {
156169
let encodedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user
@@ -167,10 +180,21 @@ final class MongoDBConnection: @unchecked Sendable {
167180
let encodedHost = host.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? host
168181
let encodedDb = database.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? database
169182

170-
uri += "\(encodedHost):\(port)"
183+
if useSrv {
184+
uri += encodedHost
185+
} else {
186+
uri += "\(encodedHost):\(port)"
187+
}
171188
uri += database.isEmpty ? "/" : "/\(encodedDb)"
172189

173-
let effectiveAuthSource = authSource.flatMap { $0.isEmpty ? nil : $0 } ?? "admin"
190+
let effectiveAuthSource: String
191+
if let source = authSource, !source.isEmpty {
192+
effectiveAuthSource = source
193+
} else if !database.isEmpty {
194+
effectiveAuthSource = database
195+
} else {
196+
effectiveAuthSource = "admin"
197+
}
174198
let encodedAuthSource = effectiveAuthSource
175199
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? effectiveAuthSource
176200
var params: [String] = [
@@ -206,6 +230,24 @@ final class MongoDBConnection: @unchecked Sendable {
206230
if let wc = writeConcern, !wc.isEmpty {
207231
params.append("w=\(wc)")
208232
}
233+
if let mechanism = authMechanism, !mechanism.isEmpty {
234+
params.append("authMechanism=\(mechanism)")
235+
}
236+
if let rs = replicaSet, !rs.isEmpty {
237+
params.append("replicaSet=\(rs)")
238+
}
239+
240+
var explicitKeys: Set<String> = [
241+
"connectTimeoutMS", "serverSelectionTimeoutMS",
242+
"authSource", "authMechanism", "replicaSet",
243+
"tls", "tlsAllowInvalidCertificates", "tlsCAFile", "tlsCertificateKeyFile"
244+
]
245+
if readPreference != nil, !readPreference!.isEmpty { explicitKeys.insert("readPreference") }
246+
if writeConcern != nil, !writeConcern!.isEmpty { explicitKeys.insert("w") }
247+
for (key, value) in extraUriParams where !explicitKeys.contains(key) {
248+
let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
249+
params.append("\(key)=\(encodedValue)")
250+
}
209251

210252
uri += "?" + params.joined(separator: "&")
211253
return uri
@@ -248,14 +290,12 @@ final class MongoDBConnection: @unchecked Sendable {
248290
}
249291

250292
self.client = newClient
251-
let versionString = self.fetchServerVersionSync()
252293

253294
self.stateLock.lock()
254-
self._cachedServerVersion = versionString
255295
self._isConnected = true
256296
self.stateLock.unlock()
257297

258-
logger.info("Connected to MongoDB \(versionString ?? "unknown")")
298+
logger.info("Connected to MongoDB at \(self.host):\(self.port)")
259299
}
260300
#else
261301
throw MongoDBError.libmongocUnavailable
@@ -341,8 +381,21 @@ final class MongoDBConnection: @unchecked Sendable {
341381

342382
func serverVersion() -> String? {
343383
stateLock.lock()
344-
defer { stateLock.unlock() }
345-
return _cachedServerVersion
384+
if let cached = _cachedServerVersion {
385+
stateLock.unlock()
386+
return cached
387+
}
388+
stateLock.unlock()
389+
390+
#if canImport(CLibMongoc)
391+
let version = queue.sync { fetchServerVersionSync() }
392+
stateLock.lock()
393+
_cachedServerVersion = version
394+
stateLock.unlock()
395+
return version
396+
#else
397+
return nil
398+
#endif
346399
}
347400
func currentDatabase() -> String { database }
348401

@@ -429,6 +482,29 @@ final class MongoDBConnection: @unchecked Sendable {
429482
#endif
430483
}
431484

485+
func estimatedDocumentCount(database: String, collection: String) async throws -> Int64 {
486+
#if canImport(CLibMongoc)
487+
resetCancellation()
488+
return try await pluginDispatchAsync(on: queue) { [self] in
489+
guard !isShuttingDown, let client = self.client else {
490+
throw MongoDBError.notConnected
491+
}
492+
try checkCancelled()
493+
let col = try getCollection(client, database: database, collection: collection)
494+
defer { mongoc_collection_destroy(col) }
495+
496+
var error = bson_error_t()
497+
let count = mongoc_collection_estimated_document_count(col, nil, nil, nil, &error)
498+
if count < 0 {
499+
throw makeError(error)
500+
}
501+
return count
502+
}
503+
#else
504+
throw MongoDBError.libmongocUnavailable
505+
#endif
506+
}
507+
432508
func insertOne(database: String, collection: String, document: String) async throws -> String? {
433509
#if canImport(CLibMongoc)
434510
resetCancellation()

Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin {
4141
.init(value: "3", label: "3"),
4242
])
4343
),
44+
ConnectionField(
45+
id: "mongoUseSrv",
46+
label: "Use SRV Record",
47+
defaultValue: "false",
48+
fieldType: .toggle,
49+
section: .advanced
50+
),
51+
ConnectionField(
52+
id: "mongoAuthMechanism",
53+
label: "Auth Mechanism",
54+
fieldType: .dropdown(options: [
55+
.init(value: "", label: "Default"),
56+
.init(value: "SCRAM-SHA-1", label: "SCRAM-SHA-1"),
57+
.init(value: "SCRAM-SHA-256", label: "SCRAM-SHA-256"),
58+
.init(value: "MONGODB-X509", label: "X.509"),
59+
.init(value: "MONGODB-AWS", label: "AWS IAM"),
60+
]),
61+
section: .authentication
62+
),
63+
ConnectionField(
64+
id: "mongoReplicaSet",
65+
label: "Replica Set",
66+
section: .advanced
67+
),
4468
]
4569

4670
// MARK: - UI/Capability Metadata

Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
3636
// MARK: - Connection Management
3737

3838
func connect() async throws {
39+
let useSrv = config.additionalFields["mongoUseSrv"] == "true"
40+
let authMechanism = config.additionalFields["mongoAuthMechanism"]
41+
let replicaSet = config.additionalFields["mongoReplicaSet"]
42+
43+
var extraParams: [String: String] = [:]
44+
for (key, value) in config.additionalFields where key.hasPrefix("mongoParam_") {
45+
let paramName = String(key.dropFirst("mongoParam_".count))
46+
if !paramName.isEmpty {
47+
extraParams[paramName] = value
48+
}
49+
}
50+
3951
let conn = MongoDBConnection(
4052
host: config.host,
4153
port: config.port,
@@ -47,7 +59,11 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
4759
sslClientCertPath: config.additionalFields["sslClientCertPath"] ?? "",
4860
authSource: config.additionalFields["mongoAuthSource"],
4961
readPreference: config.additionalFields["mongoReadPreference"],
50-
writeConcern: config.additionalFields["mongoWriteConcern"]
62+
writeConcern: config.additionalFields["mongoWriteConcern"],
63+
useSrv: useSrv,
64+
authMechanism: authMechanism,
65+
replicaSet: replicaSet,
66+
extraUriParams: extraParams
5167
)
5268

5369
try await conn.connect()
@@ -183,7 +199,7 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
183199

184200
let docs = try await conn.find(
185201
database: currentDb, collection: table,
186-
filter: "{}", sort: nil, projection: nil, skip: 0, limit: 500
202+
filter: "{}", sort: nil, projection: nil, skip: 0, limit: 50
187203
).docs
188204

189205
if docs.isEmpty {
@@ -277,7 +293,7 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
277293
throw MongoDBPluginError.notConnected
278294
}
279295

280-
let count = try await conn.countDocuments(database: currentDb, collection: table, filter: "{}")
296+
let count = try await conn.estimatedDocumentCount(database: currentDb, collection: table)
281297
return Int(count)
282298
}
283299

TablePro/AppDelegate+ConnectionHandler.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ extension AppDelegate {
440440
tagId = ConnectionURLParser.tagId(fromEnvName: envName)
441441
}
442442

443-
return DatabaseConnection(
443+
var connection = DatabaseConnection(
444444
name: parsed.connectionName ?? parsed.suggestedName,
445445
host: parsed.host,
446446
port: parsed.port ?? parsed.type.defaultPort,
@@ -452,8 +452,19 @@ extension AppDelegate {
452452
color: color,
453453
tagId: tagId,
454454
mongoAuthSource: parsed.authSource,
455+
mongoUseSrv: parsed.useSrv,
456+
mongoAuthMechanism: parsed.mongoQueryParams["authMechanism"],
457+
mongoReplicaSet: parsed.mongoQueryParams["replicaSet"],
455458
redisDatabase: parsed.redisDatabase,
456459
oracleServiceName: parsed.oracleServiceName
457460
)
461+
462+
for (key, value) in parsed.mongoQueryParams where !value.isEmpty {
463+
if key != "authMechanism" && key != "replicaSet" {
464+
connection.additionalFields["mongoParam_\(key)"] = value
465+
}
466+
}
467+
468+
return connection
458469
}
459470
}

TablePro/Core/Plugins/PluginManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,16 @@ final class PluginManager {
400400
}
401401

402402
let bundleId = bundle.bundleIdentifier ?? url.lastPathComponent
403+
404+
// Skip user-installed plugin if a built-in version already exists
405+
if source == .userInstalled,
406+
let existing = plugins.first(where: { $0.id == bundleId }),
407+
existing.source == .builtIn
408+
{
409+
Self.logger.info("Skipping user-installed '\(bundleId)' — built-in version already loaded")
410+
return existing
411+
}
412+
403413
let disabled = disabledPluginIds
404414

405415
let driverType = principalClass as? any DriverPlugin.Type

0 commit comments

Comments
 (0)