From a156b83688b78952cb4a47222e9d466fdb08b4ed Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 22 Mar 2026 16:18:04 +0700 Subject: [PATCH 1/5] fix: support mongodb+srv:// URIs, bundle plugin, skip duplicates, and speed up collection loading (#419) --- CHANGELOG.md | 5 ++ .../MongoDBConnection.swift | 65 ++++++++++++++++--- .../MongoDBDriverPlugin/MongoDBPlugin.swift | 24 +++++++ .../MongoDBPluginDriver.swift | 18 ++++- TablePro.xcodeproj/project.pbxproj | 15 +++++ TablePro/AppDelegate+ConnectionHandler.swift | 13 +++- TablePro/Core/Plugins/PluginManager.swift | 10 +++ .../Connection/ConnectionURLParser.swift | 49 +++++++++++--- .../Connection/DatabaseConnection.swift | 21 ++++++ .../Views/Connection/ConnectionFormView.swift | 16 +++++ .../Main/MainContentCommandActions.swift | 1 - 11 files changed, 216 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2664d68..30a438e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Keyboard focus navigation (Tab, Ctrl+J/K/N/P, arrow keys) for connection list, quick switcher, and database switcher +### Fixed + +- MongoDB connection failing when importing `mongodb+srv://` URIs (#419) +- Slow MongoDB collection loading by deferring buildInfo and removing duplicate listCollections + ## [0.22.1] - 2026-03-22 ### Added diff --git a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift index 22c12de8..6437f711 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift @@ -62,6 +62,10 @@ final class MongoDBConnection: @unchecked Sendable { private let authSource: String? private let readPreference: String? private let writeConcern: String? + private let useSrv: Bool + private let authMechanism: String? + private let replicaSet: String? + private let extraUriParams: [String: String] private let stateLock = NSLock() private var _isConnected: Bool = false @@ -114,7 +118,11 @@ final class MongoDBConnection: @unchecked Sendable { sslClientCertPath: String = "", authSource: String? = nil, readPreference: String? = nil, - writeConcern: String? = nil + writeConcern: String? = nil, + useSrv: Bool = false, + authMechanism: String? = nil, + replicaSet: String? = nil, + extraUriParams: [String: String] = [:] ) { self.host = host self.port = port @@ -127,6 +135,10 @@ final class MongoDBConnection: @unchecked Sendable { self.authSource = authSource self.readPreference = readPreference self.writeConcern = writeConcern + self.useSrv = useSrv + self.authMechanism = authMechanism + self.replicaSet = replicaSet + self.extraUriParams = extraUriParams } deinit { @@ -150,7 +162,8 @@ final class MongoDBConnection: @unchecked Sendable { // MARK: - URI Construction private func buildUri() -> String { - var uri = "mongodb://" + let scheme = useSrv ? "mongodb+srv" : "mongodb" + var uri = "\(scheme)://" if !user.isEmpty { let encodedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user @@ -167,10 +180,21 @@ final class MongoDBConnection: @unchecked Sendable { let encodedHost = host.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? host let encodedDb = database.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? database - uri += "\(encodedHost):\(port)" + if useSrv { + uri += encodedHost + } else { + uri += "\(encodedHost):\(port)" + } uri += database.isEmpty ? "/" : "/\(encodedDb)" - let effectiveAuthSource = authSource.flatMap { $0.isEmpty ? nil : $0 } ?? "admin" + let effectiveAuthSource: String + if let source = authSource, !source.isEmpty { + effectiveAuthSource = source + } else if !database.isEmpty { + effectiveAuthSource = database + } else { + effectiveAuthSource = "admin" + } let encodedAuthSource = effectiveAuthSource .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? effectiveAuthSource var params: [String] = [ @@ -206,6 +230,22 @@ final class MongoDBConnection: @unchecked Sendable { if let wc = writeConcern, !wc.isEmpty { params.append("w=\(wc)") } + if let mechanism = authMechanism, !mechanism.isEmpty { + params.append("authMechanism=\(mechanism)") + } + if let rs = replicaSet, !rs.isEmpty { + params.append("replicaSet=\(rs)") + } + + let explicitKeys: Set = [ + "authSource", "readPreference", "w", "authMechanism", "replicaSet", + "connectTimeoutMS", "serverSelectionTimeoutMS", "tls", + "tlsAllowInvalidCertificates", "tlsCAFile", "tlsCertificateKeyFile" + ] + for (key, value) in extraUriParams where !explicitKeys.contains(key) { + let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value + params.append("\(key)=\(encodedValue)") + } uri += "?" + params.joined(separator: "&") return uri @@ -248,14 +288,12 @@ final class MongoDBConnection: @unchecked Sendable { } self.client = newClient - let versionString = self.fetchServerVersionSync() self.stateLock.lock() - self._cachedServerVersion = versionString self._isConnected = true self.stateLock.unlock() - logger.info("Connected to MongoDB \(versionString ?? "unknown")") + logger.info("Connected to MongoDB at \(self.host):\(self.port)") } #else throw MongoDBError.libmongocUnavailable @@ -341,8 +379,17 @@ final class MongoDBConnection: @unchecked Sendable { func serverVersion() -> String? { stateLock.lock() - defer { stateLock.unlock() } - return _cachedServerVersion + if let cached = _cachedServerVersion { + stateLock.unlock() + return cached + } + stateLock.unlock() + + let version = fetchServerVersionSync() + stateLock.lock() + _cachedServerVersion = version + stateLock.unlock() + return version } func currentDatabase() -> String { database } diff --git a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift index a7b01ef3..97d20177 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift @@ -41,6 +41,30 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin { .init(value: "3", label: "3"), ]) ), + ConnectionField( + id: "mongoUseSrv", + label: "Use SRV Record", + defaultValue: "false", + fieldType: .toggle, + section: .advanced + ), + ConnectionField( + id: "mongoAuthMechanism", + label: "Auth Mechanism", + fieldType: .dropdown(options: [ + .init(value: "", label: "Default"), + .init(value: "SCRAM-SHA-1", label: "SCRAM-SHA-1"), + .init(value: "SCRAM-SHA-256", label: "SCRAM-SHA-256"), + .init(value: "MONGODB-X509", label: "X.509"), + .init(value: "MONGODB-AWS", label: "AWS IAM"), + ]), + section: .authentication + ), + ConnectionField( + id: "mongoReplicaSet", + label: "Replica Set", + section: .advanced + ), ] // MARK: - UI/Capability Metadata diff --git a/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift b/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift index d0f1b76d..c0f125c2 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift @@ -36,6 +36,18 @@ final class MongoDBPluginDriver: PluginDatabaseDriver { // MARK: - Connection Management func connect() async throws { + let useSrv = config.additionalFields["mongoUseSrv"] == "true" + let authMechanism = config.additionalFields["mongoAuthMechanism"] + let replicaSet = config.additionalFields["mongoReplicaSet"] + + var extraParams: [String: String] = [:] + for (key, value) in config.additionalFields where key.hasPrefix("mongoParam_") { + let paramName = String(key.dropFirst("mongoParam_".count)) + if !paramName.isEmpty { + extraParams[paramName] = value + } + } + let conn = MongoDBConnection( host: config.host, port: config.port, @@ -47,7 +59,11 @@ final class MongoDBPluginDriver: PluginDatabaseDriver { sslClientCertPath: config.additionalFields["sslClientCertPath"] ?? "", authSource: config.additionalFields["mongoAuthSource"], readPreference: config.additionalFields["mongoReadPreference"], - writeConcern: config.additionalFields["mongoWriteConcern"] + writeConcern: config.additionalFields["mongoWriteConcern"], + useSrv: useSrv, + authMechanism: authMechanism, + replicaSet: replicaSet, + extraUriParams: extraParams ) try await conn.connect() diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index f552dc24..5ad5fad2 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000007 /* CodeEditTextView */; }; 5ACE00012F4F00000000000A /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000009 /* Sparkle */; }; 5ACE00012F4F00000000000D /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000C /* MarkdownUI */; }; + 5A866000D00000000 /* MongoDBDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A866000100000000 /* MongoDBDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5ADDB00000000000000000D0 /* DynamoDBDriverPlugin.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5ADDB00300000000000000A0 /* DynamoDBDriverPlugin.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5ADDB00100000000000000A1 /* DynamoDBConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A1 /* DynamoDBConnection.swift */; }; 5ADDB00100000000000000A2 /* DynamoDBItemFlattener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A2 /* DynamoDBItemFlattener.swift */; }; @@ -91,6 +92,13 @@ remoteGlobalIDString = 5A865000000000000; remoteInfo = MySQLDriver; }; + 5A866000B00000000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5A866000000000000; + remoteInfo = MongoDBDriver; + }; 5A868000B00000000 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; @@ -156,6 +164,7 @@ 5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */, 5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */, 5ADDB00000000000000000D0 /* DynamoDBDriverPlugin.tableplugin in Copy Plug-Ins */, + 5A866000D00000000 /* MongoDBDriver.tableplugin in Copy Plug-Ins */, ); name = "Copy Plug-Ins"; runOnlyForDeploymentPostprocessing = 0; @@ -824,6 +833,7 @@ 5A862000C00000000 /* PBXTargetDependency */, 5A863000C00000000 /* PBXTargetDependency */, 5A865000C00000000 /* PBXTargetDependency */, + 5A866000C00000000 /* PBXTargetDependency */, 5A868000C00000000 /* PBXTargetDependency */, 5A869000C00000000 /* PBXTargetDependency */, 5A86A000C00000000 /* PBXTargetDependency */, @@ -1746,6 +1756,11 @@ target = 5A865000000000000 /* MySQLDriver */; targetProxy = 5A865000B00000000 /* PBXContainerItemProxy */; }; + 5A866000C00000000 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5A866000000000000 /* MongoDBDriver */; + targetProxy = 5A866000B00000000 /* PBXContainerItemProxy */; + }; 5A868000C00000000 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5A868000000000000 /* PostgreSQLDriver */; diff --git a/TablePro/AppDelegate+ConnectionHandler.swift b/TablePro/AppDelegate+ConnectionHandler.swift index 0fe711d1..6c696afc 100644 --- a/TablePro/AppDelegate+ConnectionHandler.swift +++ b/TablePro/AppDelegate+ConnectionHandler.swift @@ -440,7 +440,7 @@ extension AppDelegate { tagId = ConnectionURLParser.tagId(fromEnvName: envName) } - return DatabaseConnection( + var connection = DatabaseConnection( name: parsed.connectionName ?? parsed.suggestedName, host: parsed.host, port: parsed.port ?? parsed.type.defaultPort, @@ -452,8 +452,19 @@ extension AppDelegate { color: color, tagId: tagId, mongoAuthSource: parsed.authSource, + mongoUseSrv: parsed.useSrv, + mongoAuthMechanism: parsed.mongoQueryParams["authMechanism"], + mongoReplicaSet: parsed.mongoQueryParams["replicaSet"], redisDatabase: parsed.redisDatabase, oracleServiceName: parsed.oracleServiceName ) + + for (key, value) in parsed.mongoQueryParams where !value.isEmpty { + if key != "authMechanism" && key != "replicaSet" { + connection.additionalFields["mongoParam_\(key)"] = value + } + } + + return connection } } diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 7f8ec9c5..d058d691 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -400,6 +400,16 @@ final class PluginManager { } let bundleId = bundle.bundleIdentifier ?? url.lastPathComponent + + // Skip user-installed plugin if a built-in version already exists + if source == .userInstalled, + let existing = plugins.first(where: { $0.id == bundleId }), + existing.source == .builtIn + { + Self.logger.info("Skipping user-installed '\(bundleId)' — built-in version already loaded") + return existing + } + let disabled = disabledPluginIds let driverType = principalClass as? any DriverPlugin.Type diff --git a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift index 2857b58f..76942c54 100644 --- a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift +++ b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift @@ -32,6 +32,8 @@ struct ParsedConnectionURL { let filterValue: String? let filterCondition: String? let oracleServiceName: String? + let useSrv: Bool + let mongoQueryParams: [String: String] var suggestedName: String { if let connectionName, !connectionName.isEmpty { @@ -122,6 +124,8 @@ struct ConnectionURLParser { } } + let isSrv = scheme == "mongodb+srv" + if dbType == .sqlite { let path = String(trimmed[schemeEnd.upperBound...]) return .success(ParsedConnectionURL( @@ -150,7 +154,9 @@ struct ConnectionURLParser { filterOperation: nil, filterValue: nil, filterCondition: nil, - oracleServiceName: nil + oracleServiceName: nil, + useSrv: false, + mongoQueryParams: [:] )) } @@ -181,7 +187,7 @@ struct ConnectionURLParser { database = String(database.dropFirst()) } - let ext = parseQueryItems(components.queryItems) + var ext = parseQueryItems(components.queryItems, dbType: dbType) var sslMode = ext.sslMode // Redis-specific: parse database index from path and handle TLS scheme @@ -203,10 +209,19 @@ struct ConnectionURLParser { database = "" } + // SRV implies TLS and no explicit port + if isSrv { + ext.useSrv = true + if sslMode == nil { + sslMode = .required + } + } + let effectivePort = isSrv ? nil : port + return .success(ParsedConnectionURL( type: dbType, host: host, - port: port, + port: effectivePort, database: database, username: username, password: password, @@ -229,7 +244,9 @@ struct ConnectionURLParser { filterOperation: ext.filterOperation, filterValue: ext.filterValue, filterCondition: ext.filterCondition, - oracleServiceName: oracleServiceName + oracleServiceName: oracleServiceName, + useSrv: ext.useSrv, + mongoQueryParams: ext.mongoQueryParams )) } @@ -360,7 +377,9 @@ struct ConnectionURLParser { filterOperation: ext.filterOperation, filterValue: ext.filterValue, filterCondition: ext.filterCondition, - oracleServiceName: oracleServiceName + oracleServiceName: oracleServiceName, + useSrv: ext.useSrv, + mongoQueryParams: ext.mongoQueryParams )) } @@ -382,14 +401,16 @@ struct ConnectionURLParser { var filterOperation: String? var filterValue: String? var filterCondition: String? + var useSrv: Bool = false + var mongoQueryParams: [String: String] = [:] } - private static func parseQueryItems(_ queryItems: [URLQueryItem]?) -> ExtendedParams { + private static func parseQueryItems(_ queryItems: [URLQueryItem]?, dbType: DatabaseType? = nil) -> ExtendedParams { var ext = ExtendedParams() guard let queryItems else { return ext } for item in queryItems { guard let value = item.value, !value.isEmpty else { continue } - applyQueryParam(key: item.name, value: value, to: &ext) + applyQueryParam(key: item.name, value: value, to: &ext, dbType: dbType) } return ext } @@ -420,7 +441,7 @@ struct ConnectionURLParser { return ext } - private static func applyQueryParam(key: String, value: String, to ext: inout ExtendedParams) { + private static func applyQueryParam(key: String, value: String, to ext: inout ExtendedParams, dbType: DatabaseType? = nil) { switch key { case "sslmode": ext.sslMode = parseSSLMode(value) @@ -456,8 +477,18 @@ struct ConnectionURLParser { if ext.sslMode == nil, let intValue = Int(value) { ext.sslMode = parseTlsModeInteger(intValue) } + case "tls", "ssl": + if value.lowercased() == "true" && ext.sslMode == nil { + ext.sslMode = .required + } + case "authMechanism", "authmechanism": + ext.mongoQueryParams["authMechanism"] = value + case "replicaSet", "replicaset": + ext.mongoQueryParams["replicaSet"] = value default: - break + if dbType == .mongodb { + ext.mongoQueryParams[key] = value + } } } diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index 89aaf135..f0b17a8b 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -398,6 +398,21 @@ struct DatabaseConnection: Identifiable, Hashable { set { additionalFields["mongoWriteConcern"] = newValue ?? "" } } + var mongoUseSrv: Bool { + get { additionalFields["mongoUseSrv"] == "true" } + set { additionalFields["mongoUseSrv"] = newValue ? "true" : "" } + } + + var mongoAuthMechanism: String? { + get { additionalFields["mongoAuthMechanism"]?.nilIfEmpty } + set { additionalFields["mongoAuthMechanism"] = newValue ?? "" } + } + + var mongoReplicaSet: String? { + get { additionalFields["mongoReplicaSet"]?.nilIfEmpty } + set { additionalFields["mongoReplicaSet"] = newValue ?? "" } + } + var mssqlSchema: String? { get { additionalFields["mssqlSchema"]?.nilIfEmpty } set { additionalFields["mssqlSchema"] = newValue ?? "" } @@ -437,6 +452,9 @@ struct DatabaseConnection: Identifiable, Hashable { mongoAuthSource: String? = nil, mongoReadPreference: String? = nil, mongoWriteConcern: String? = nil, + mongoUseSrv: Bool = false, + mongoAuthMechanism: String? = nil, + mongoReplicaSet: String? = nil, redisDatabase: Int? = nil, mssqlSchema: String? = nil, oracleServiceName: String? = nil, @@ -467,6 +485,9 @@ struct DatabaseConnection: Identifiable, Hashable { if let v = mongoAuthSource { fields["mongoAuthSource"] = v } if let v = mongoReadPreference { fields["mongoReadPreference"] = v } if let v = mongoWriteConcern { fields["mongoWriteConcern"] = v } + if mongoUseSrv { fields["mongoUseSrv"] = "true" } + if let v = mongoAuthMechanism { fields["mongoAuthMechanism"] = v } + if let v = mongoReplicaSet { fields["mongoReplicaSet"] = v } if let v = mssqlSchema { fields["mssqlSchema"] = v } if let v = oracleServiceName { fields["oracleServiceName"] = v } self.additionalFields = fields diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index 9514b284..e6bd9598 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -1545,6 +1545,22 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length if let authSourceValue = parsed.authSource, !authSourceValue.isEmpty { additionalFieldValues["mongoAuthSource"] = authSourceValue } + if parsed.useSrv { + additionalFieldValues["mongoUseSrv"] = "true" + if sslMode == .disabled { + sslMode = .required + } + } + for (key, value) in parsed.mongoQueryParams where !value.isEmpty { + switch key { + case "authMechanism": + additionalFieldValues["mongoAuthMechanism"] = value + case "replicaSet": + additionalFieldValues["mongoReplicaSet"] = value + default: + additionalFieldValues["mongoParam_\(key)"] = value + } + } if parsed.type.pluginTypeId == "Redis", !parsed.database.isEmpty { additionalFieldValues["redisDatabase"] = parsed.database } diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index 9b1dc281..e1626e2d 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -541,7 +541,6 @@ final class MainContentCommandActions { private func handleDatabaseDidConnect() { Task { @MainActor in - await coordinator?.loadSchema() if let driver = DatabaseManager.shared.driver(for: self.connection.id) { coordinator?.toolbarState.databaseVersion = driver.serverVersion } From d5b0f10a7f29d27bc270fa2ff5573e9071fcd303 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 22 Mar 2026 16:24:58 +0700 Subject: [PATCH 2/5] perf: reduce MongoDB collection load time with estimatedDocumentCount and smaller schema sample --- .../MongoDBConnection.swift | 23 +++++++++++++++++++ .../MongoDBPluginDriver.swift | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift index 6437f711..aacd6167 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift @@ -476,6 +476,29 @@ final class MongoDBConnection: @unchecked Sendable { #endif } + func estimatedDocumentCount(database: String, collection: String) async throws -> Int64 { + #if canImport(CLibMongoc) + resetCancellation() + return try await pluginDispatchAsync(on: queue) { [self] in + guard !isShuttingDown, let client = self.client else { + throw MongoDBError.notConnected + } + try checkCancelled() + let col = try getCollection(client, database: database, collection: collection) + defer { mongoc_collection_destroy(col) } + + var error = bson_error_t() + let count = mongoc_collection_estimated_document_count(col, nil, nil, nil, &error) + if count < 0 { + throw makeError(error) + } + return count + } + #else + throw MongoDBError.libmongocUnavailable + #endif + } + func insertOne(database: String, collection: String, document: String) async throws -> String? { #if canImport(CLibMongoc) resetCancellation() diff --git a/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift b/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift index c0f125c2..c9e3432e 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift @@ -199,7 +199,7 @@ final class MongoDBPluginDriver: PluginDatabaseDriver { let docs = try await conn.find( database: currentDb, collection: table, - filter: "{}", sort: nil, projection: nil, skip: 0, limit: 500 + filter: "{}", sort: nil, projection: nil, skip: 0, limit: 50 ).docs if docs.isEmpty { @@ -293,7 +293,7 @@ final class MongoDBPluginDriver: PluginDatabaseDriver { throw MongoDBPluginError.notConnected } - let count = try await conn.countDocuments(database: currentDb, collection: table, filter: "{}") + let count = try await conn.estimatedDocumentCount(database: currentDb, collection: table) return Int(count) } From 00c78ac9a6a5dc6a0d134d59f5e0632b63e6db26 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 22 Mar 2026 16:31:27 +0700 Subject: [PATCH 3/5] revert: remove MongoDB plugin from built-in app bundle --- TablePro.xcodeproj/project.pbxproj | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index 5ad5fad2..f552dc24 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ 5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000007 /* CodeEditTextView */; }; 5ACE00012F4F00000000000A /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000009 /* Sparkle */; }; 5ACE00012F4F00000000000D /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000C /* MarkdownUI */; }; - 5A866000D00000000 /* MongoDBDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A866000100000000 /* MongoDBDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5ADDB00000000000000000D0 /* DynamoDBDriverPlugin.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5ADDB00300000000000000A0 /* DynamoDBDriverPlugin.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5ADDB00100000000000000A1 /* DynamoDBConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A1 /* DynamoDBConnection.swift */; }; 5ADDB00100000000000000A2 /* DynamoDBItemFlattener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADDB00200000000000000A2 /* DynamoDBItemFlattener.swift */; }; @@ -92,13 +91,6 @@ remoteGlobalIDString = 5A865000000000000; remoteInfo = MySQLDriver; }; - 5A866000B00000000 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5A866000000000000; - remoteInfo = MongoDBDriver; - }; 5A868000B00000000 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; @@ -164,7 +156,6 @@ 5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */, 5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */, 5ADDB00000000000000000D0 /* DynamoDBDriverPlugin.tableplugin in Copy Plug-Ins */, - 5A866000D00000000 /* MongoDBDriver.tableplugin in Copy Plug-Ins */, ); name = "Copy Plug-Ins"; runOnlyForDeploymentPostprocessing = 0; @@ -833,7 +824,6 @@ 5A862000C00000000 /* PBXTargetDependency */, 5A863000C00000000 /* PBXTargetDependency */, 5A865000C00000000 /* PBXTargetDependency */, - 5A866000C00000000 /* PBXTargetDependency */, 5A868000C00000000 /* PBXTargetDependency */, 5A869000C00000000 /* PBXTargetDependency */, 5A86A000C00000000 /* PBXTargetDependency */, @@ -1756,11 +1746,6 @@ target = 5A865000000000000 /* MySQLDriver */; targetProxy = 5A865000B00000000 /* PBXContainerItemProxy */; }; - 5A866000C00000000 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 5A866000000000000 /* MongoDBDriver */; - targetProxy = 5A866000B00000000 /* PBXContainerItemProxy */; - }; 5A868000C00000000 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5A868000000000000 /* PostgreSQLDriver */; From 8565c783d6240a13fdab81c3f950b5788e170b4d Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 22 Mar 2026 16:39:07 +0700 Subject: [PATCH 4/5] docs: fix CHANGELOG entries to use Added/Changed instead of Fixed for unreleased features --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a438e9..e82da0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Keyboard focus navigation (Tab, Ctrl+J/K/N/P, arrow keys) for connection list, quick switcher, and database switcher +- MongoDB `mongodb+srv://` URI support with SRV toggle, Auth Mechanism dropdown, and Replica Set field (#419) -### Fixed +### Changed -- MongoDB connection failing when importing `mongodb+srv://` URIs (#419) -- Slow MongoDB collection loading by deferring buildInfo and removing duplicate listCollections +- MongoDB `authSource` defaults to database name per MongoDB URI spec instead of always "admin" +- MongoDB collection loading uses `estimatedDocumentCount` and smaller schema sample for faster sidebar population ## [0.22.1] - 2026-03-22 From 4c339f82c0304f3a09e6ceb8c5cd02a317b2fac5 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 22 Mar 2026 16:42:05 +0700 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?serverVersion=20queue=20dispatch,=20explicitKeys=20filter,=20SS?= =?UTF-8?q?H=20param=20forwarding,=20stale=20field=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MongoDBDriverPlugin/MongoDBConnection.swift | 16 +++++++++++----- .../Connection/ConnectionURLParser.swift | 6 +++--- .../Views/Connection/ConnectionFormView.swift | 7 +++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift index aacd6167..69b7a00f 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBConnection.swift @@ -237,11 +237,13 @@ final class MongoDBConnection: @unchecked Sendable { params.append("replicaSet=\(rs)") } - let explicitKeys: Set = [ - "authSource", "readPreference", "w", "authMechanism", "replicaSet", - "connectTimeoutMS", "serverSelectionTimeoutMS", "tls", - "tlsAllowInvalidCertificates", "tlsCAFile", "tlsCertificateKeyFile" + var explicitKeys: Set = [ + "connectTimeoutMS", "serverSelectionTimeoutMS", + "authSource", "authMechanism", "replicaSet", + "tls", "tlsAllowInvalidCertificates", "tlsCAFile", "tlsCertificateKeyFile" ] + if readPreference != nil, !readPreference!.isEmpty { explicitKeys.insert("readPreference") } + if writeConcern != nil, !writeConcern!.isEmpty { explicitKeys.insert("w") } for (key, value) in extraUriParams where !explicitKeys.contains(key) { let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value params.append("\(key)=\(encodedValue)") @@ -385,11 +387,15 @@ final class MongoDBConnection: @unchecked Sendable { } stateLock.unlock() - let version = fetchServerVersionSync() + #if canImport(CLibMongoc) + let version = queue.sync { fetchServerVersionSync() } stateLock.lock() _cachedServerVersion = version stateLock.unlock() return version + #else + return nil + #endif } func currentDatabase() -> String { database } diff --git a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift index 76942c54..bb5b19e8 100644 --- a/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift +++ b/TablePro/Core/Utilities/Connection/ConnectionURLParser.swift @@ -342,7 +342,7 @@ struct ConnectionURLParser { host = "127.0.0.1" } - let ext = parseSSHQueryString(queryString) + let ext = parseSSHQueryString(queryString, dbType: dbType) // Oracle-specific: path component is the service name, not the database name var oracleServiceName: String? @@ -415,7 +415,7 @@ struct ConnectionURLParser { return ext } - private static func parseSSHQueryString(_ queryString: String?) -> ExtendedParams { + private static func parseSSHQueryString(_ queryString: String?, dbType: DatabaseType? = nil) -> ExtendedParams { var ext = ExtendedParams() guard let queryString else { return ext } let params = queryString.split(separator: "&", omittingEmptySubsequences: true) @@ -436,7 +436,7 @@ struct ConnectionURLParser { ext.agentSocket = value.removingPercentEncoding ?? value continue } - applyQueryParam(key: String(key), value: value, to: &ext) + applyQueryParam(key: String(key), value: value, to: &ext, dbType: dbType) } return ext } diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index e6bd9598..56e49508 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -1542,6 +1542,13 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length applySSHAgentSocketPath(parsed.agentSocket ?? "") } } + // Clear stale MongoDB fields before applying new import + let mongoKeys = additionalFieldValues.keys.filter { + $0.hasPrefix("mongo") || $0.hasPrefix("mongoParam_") + } + for key in mongoKeys { + additionalFieldValues.removeValue(forKey: key) + } if let authSourceValue = parsed.authSource, !authSourceValue.isEmpty { additionalFieldValues["mongoAuthSource"] = authSourceValue }