Skip to content

Commit 7aea300

Browse files
authored
SWIFT-1322 Support batchSize option for listCollections (#673)
1 parent 64931e2 commit 7aea300

File tree

6 files changed

+107
-12
lines changed

6 files changed

+107
-12
lines changed

Sources/MongoSwift/MongoCursor.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ public class MongoCursor<T: Codable>: CursorProtocol {
4949
public let id: Int64?
5050

5151
/**
52-
* Initializes a new `MongoCursor` instance. Not meant to be instantiated directly by a user. When `forceIO` is
53-
* true, this initializer will force a connection to the server if one is not already established.
52+
* Initializes a new `MongoCursor` instance. Not meant to be instantiated directly by a user.
5453
*
5554
* - Throws:
5655
* - `MongoError.InvalidArgumentError` if the options passed to the command that generated this cursor formed an

Sources/MongoSwift/MongoError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ internal func toErrorString(_ error: bson_error_t) -> String {
522522
}
523523
}
524524

525-
internal let failedToRetrieveCursorMessage = "Couldn't get cursor from the server"
525+
internal let failedToRetrieveCursorMessage = "Expected libmongoc to return a cursor, unexpectedly got nil"
526526

527527
extension MongoErrorProtocol {
528528
/// Determines whether this error is an "ns not found" error.

Sources/MongoSwift/Operations/ListCollectionsOperation.swift

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,61 @@ internal struct ListCollectionsOperation: Operation {
106106
}
107107

108108
internal func execute(using connection: Connection, session: ClientSession?) throws -> ListCollectionsResults {
109-
var opts = try encodeOptions(options: self.options, session: session) ?? BSONDocument()
110-
opts["nameOnly"] = .bool(self.nameOnly)
109+
// Drivers MUST run listCollections on the primary node when in a replica set topology, unless directly
110+
// connected to a secondary node in Single topology.
111+
let readPref = ReadPreference.primary
112+
113+
var cmd: BSONDocument = ["listCollections": 1, "nameOnly": .bool(self.nameOnly)]
111114
if let filterDoc = self.filter {
112-
opts["filter"] = .document(filterDoc)
115+
cmd["filter"] = .document(filterDoc)
113116

114117
// If `listCollectionNames` is called with a non-name filter key, change server-side nameOnly flag to false.
118+
// per spec: drivers MUST NOT set nameOnly if a filter specifies any keys other than name.
115119
if self.nameOnly && filterDoc.keys.contains(where: { $0 != "name" }) {
116-
opts["nameOnly"] = false
120+
cmd["nameOnly"] = false
121+
}
122+
}
123+
124+
var cursorOpts: BSONDocument = [:]
125+
126+
if let batchSize = options?.batchSize {
127+
guard let i32 = Int32(exactly: batchSize) else {
128+
throw MongoError.InvalidArgumentError(
129+
message: "batchSize option must be representable as an Int32. Got: \(batchSize)"
130+
)
131+
}
132+
cursorOpts = ["batchSize": .int32(i32)]
133+
}
134+
135+
let commandOpts = try encodeOptions(options: nil as BSONDocument?, session: session) ?? BSONDocument()
136+
cursorOpts = try encodeOptions(options: cursorOpts, session: session) ?? BSONDocument()
137+
cmd["cursor"] = .document(cursorOpts)
138+
139+
// We don't need to clean up this reply ourselves, as `mongoc_cursor_new_from_command_reply_with_opts` will
140+
// consume it.
141+
var reply = try self.database.withMongocDatabase(from: connection) { dbPtr in
142+
try readPref.withMongocReadPreference { rpPtr in
143+
try runMongocCommandWithCReply(
144+
command: cmd,
145+
options: commandOpts
146+
) { cmdPtr, optsPtr, replyPtr, error in
147+
mongoc_database_read_command_with_opts(dbPtr, cmdPtr, rpPtr, optsPtr, replyPtr, &error)
148+
}
117149
}
118150
}
119151

120-
let collections: OpaquePointer = self.database.withMongocDatabase(from: connection) { dbPtr in
121-
opts.withBSONPointer { optsPtr in
122-
guard let collections = mongoc_database_find_collections_with_opts(dbPtr, optsPtr) else {
123-
fatalError(failedToRetrieveCursorMessage)
152+
let collections = connection.withMongocConnection { connPtr in
153+
withUnsafeMutablePointer(to: &reply) { replyPtr -> OpaquePointer in
154+
withOptionalBSONPointer(to: cursorOpts) { cursorOptsPtr in
155+
guard let result = mongoc_cursor_new_from_command_reply_with_opts(
156+
connPtr,
157+
replyPtr,
158+
cursorOptsPtr
159+
) else {
160+
fatalError(failedToRetrieveCursorMessage)
161+
}
162+
return result
124163
}
125-
return collections
126164
}
127165
}
128166

Sources/MongoSwift/Operations/MongocCommandHelpers.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,33 @@ internal func runMongocCommandWithReply(
3333
}
3434
}
3535

36+
/**
37+
* Calls the provided mongoc command method using pointers to the specified command and options. Returns the resulting
38+
* filled-out `bson_t` from libmongoc.
39+
* The caller of this method is responsible for ensuring the reply `bson_t` is properly cleaned up.
40+
* If you need a `BSONDocument` reply, use `runMongocCommandWithReply` instead.
41+
* If you don't need the reply, use `runMongocCommand` instead.
42+
*/
43+
internal func runMongocCommandWithCReply(
44+
command: BSONDocument,
45+
options: BSONDocument?,
46+
body: MongocCommandFunc
47+
) throws -> bson_t {
48+
var reply = bson_t()
49+
do {
50+
try withUnsafeMutablePointer(to: &reply) { replyPtr in
51+
try _runMongocCommand(command: command, options: options, replyPtr: replyPtr, body: body)
52+
}
53+
} catch {
54+
// on error, we need to clean up the bson_t ourselves here. on success, it is the caller's responsibility.
55+
withUnsafeMutablePointer(to: &reply) { ptr in
56+
bson_destroy(ptr)
57+
}
58+
throw error
59+
}
60+
return reply
61+
}
62+
3663
/// Calls the provided mongoc command method using pointers to the specified command and options.
3764
internal func runMongocCommand(command: BSONDocument, options: BSONDocument?, body: MongocCommandFunc) throws {
3865
try withStackAllocatedMutableBSONPointer { replyPtr in

Tests/LinuxMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ extension MongoDatabaseTests {
230230
("testDropDatabase", testDropDatabase),
231231
("testCreateCollection", testCreateCollection),
232232
("testListCollections", testListCollections),
233+
("testListCollectionsBatchSize", testListCollectionsBatchSize),
233234
("testAggregate", testAggregate),
234235
("testAggregateWithOutputType", testAggregateWithOutputType),
235236
("testAggregateWithListLocalSessions", testAggregateWithListLocalSessions),

Tests/MongoSwiftSyncTests/MongoDatabaseTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22
import MongoSwiftSync
33
import Nimble
44
import TestsCommon
5+
import XCTest
56

67
final class MongoDatabaseTests: MongoSwiftTestCase {
78
override func setUp() {
@@ -180,6 +181,35 @@ final class MongoDatabaseTests: MongoSwiftTestCase {
180181
expect(listNamesEvents[3].command["nameOnly"]).to(equal(false))
181182
}
182183

184+
func testListCollectionsBatchSize() throws {
185+
try self.withTestNamespace { client, db, _ in
186+
// clear out collections
187+
try db.drop()
188+
189+
_ = try db.createCollection("foo")
190+
_ = try db.createCollection("bar")
191+
_ = try db.createCollection("baz")
192+
193+
let monitor = client.addCommandMonitor()
194+
try monitor.captureEvents {
195+
let options = ListCollectionsOptions(batchSize: 2)
196+
_ = Array(try db.listCollections(options: options))
197+
}
198+
199+
let events = monitor.commandStartedEvents(withNames: ["listCollections", "getMore"])
200+
201+
guard events.count == 2 else {
202+
XCTFail("Expected to find 2 events, but got \(events.count): \(events)")
203+
return
204+
}
205+
206+
expect(events[0].commandName).to(equal("listCollections"))
207+
expect(events[0].command["cursor"]?.documentValue?["batchSize"]?.toInt()).to(equal(2))
208+
expect(events[1].commandName).to(equal("getMore"))
209+
expect(events[1].command["batchSize"]?.toInt()).to(equal(2))
210+
}
211+
}
212+
183213
func testAggregate() throws {
184214
let client = try MongoClient.makeTestClient()
185215
// $currentOp must be run on the admin database

0 commit comments

Comments
 (0)