Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
datlechin marked this conversation as resolved.
Outdated

## [0.22.1] - 2026-03-22

### Added
Expand Down
65 changes: 56 additions & 9 deletions Plugins/MongoDBDriverPlugin/MongoDBConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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] = [
Expand Down Expand Up @@ -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<String> = [
"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)")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

uri += "?" + params.joined(separator: "&")
return uri
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
func currentDatabase() -> String { database }

Expand Down
24 changes: 24 additions & 0 deletions Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -824,6 +833,7 @@
5A862000C00000000 /* PBXTargetDependency */,
5A863000C00000000 /* PBXTargetDependency */,
5A865000C00000000 /* PBXTargetDependency */,
5A866000C00000000 /* PBXTargetDependency */,
5A868000C00000000 /* PBXTargetDependency */,
5A869000C00000000 /* PBXTargetDependency */,
5A86A000C00000000 /* PBXTargetDependency */,
Expand Down Expand Up @@ -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 */;
Expand Down
13 changes: 12 additions & 1 deletion TablePro/AppDelegate+ConnectionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
}
10 changes: 10 additions & 0 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading