Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
88 changes: 79 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 Expand Up @@ -429,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()
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
22 changes: 19 additions & 3 deletions 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 Expand Up @@ -183,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 {
Expand Down Expand Up @@ -277,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)
}

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