Skip to content

Commit bc95b1c

Browse files
kaywuxkmahar
authored andcommitted
SWIFT-133 Implement new Count API (#331)
1 parent 8c9a4b4 commit bc95b1c

15 files changed

+265
-106
lines changed

Sources/MongoSwift/MongoCollection+Read.swift

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -79,50 +79,27 @@ extension SyncMongoCollection {
7979
}
8080
}
8181

82-
// TODO: SWIFT-133: mark this method deprecated https://jira.mongodb.org/browse/SWIFT-133
8382
/**
84-
* Counts the number of documents in this collection matching the provided filter.
83+
* Counts the number of documents in this collection matching the provided filter. Note that an empty filter will
84+
* force a scan of the entire collection. For a fast count of the total documents in a collection see
85+
* `estimatedDocumentCount`.
8586
*
8687
* - Parameters:
8788
* - filter: a `Document`, the filter that documents must match in order to be counted
88-
* - options: Optional `CountOptions` to use when executing the command
89+
* - options: Optional `CountDocumentsOptions` to use when executing the command
8990
* - session: Optional `SyncClientSession` to use when executing this command
9091
*
9192
* - Returns: The count of the documents that matched the filter
92-
*
93-
* - Throws:
94-
* - `ServerError.commandError` if an error occurs that prevents the command from performing the write.
95-
* - `UserError.invalidArgumentError` if the options passed in form an invalid combination.
96-
* - `EncodingError` if an error occurs while encoding the options to BSON.
9793
*/
98-
public func count(
94+
public func countDocuments(
9995
_ filter: Document = [:],
100-
options: CountOptions? = nil,
96+
options: CountDocumentsOptions? = nil,
10197
session: SyncClientSession? = nil
10298
) throws -> Int {
103-
let operation = CountOperation(collection: self, filter: filter, options: options)
99+
let operation = CountDocumentsOperation(collection: self, filter: filter, options: options)
104100
return try self._client.executeOperation(operation, session: session)
105101
}
106102

107-
/**
108-
* Counts the number of documents in this collection matching the provided filter.
109-
*
110-
* - Parameters:
111-
* - filter: a `Document`, the filter that documents must match in order to be counted
112-
* - options: Optional `CountDocumentsOptions` to use when executing the command
113-
* - session: Optional `SyncClientSession` to use when executing this command
114-
*
115-
* - Returns: The count of the documents that matched the filter
116-
*/
117-
private func countDocuments(
118-
_: Document = [:],
119-
options _: CountDocumentsOptions? = nil,
120-
session _: SyncClientSession? = nil
121-
) throws -> Int {
122-
// TODO: SWIFT-133: implement this https://jira.mongodb.org/browse/SWIFT-133
123-
throw UserError.logicError(message: "Unimplemented command")
124-
}
125-
126103
/**
127104
* Gets an estimate of the count of documents in this collection using collection metadata.
128105
*
@@ -132,12 +109,12 @@ extension SyncMongoCollection {
132109
*
133110
* - Returns: an estimate of the count of documents in this collection
134111
*/
135-
private func estimatedDocumentCount(
136-
options _: EstimatedDocumentCountOptions? = nil,
137-
session _: SyncClientSession? = nil
112+
public func estimatedDocumentCount(
113+
options: EstimatedDocumentCountOptions? = nil,
114+
session: SyncClientSession? = nil
138115
) throws -> Int {
139-
// TODO: SWIFT-133: implement this https://jira.mongodb.org/browse/SWIFT-133
140-
throw UserError.logicError(message: "Unimplemented command")
116+
let operation = EstimatedDocumentCountOperation(collection: self, options: options)
117+
return try self._client.executeOperation(operation, session: session)
141118
}
142119

143120
/**
@@ -263,21 +240,6 @@ public struct AggregateOptions: Codable {
263240
}
264241
}
265242

266-
/// The `countDocuments` command takes the same options as the deprecated `count`.
267-
private typealias CountDocumentsOptions = CountOptions
268-
269-
/// Options to use when executing an `estimatedDocumentCount` command on a `MongoCollection` or a
270-
/// `SyncMongoCollection`.
271-
private struct EstimatedDocumentCountOptions {
272-
/// The maximum amount of time to allow the query to run.
273-
public let maxTimeMS: Int64?
274-
275-
/// Initializer allowing any/all parameters to be omitted or optional.
276-
public init(maxTimeMS: Int64? = nil) {
277-
self.maxTimeMS = maxTimeMS
278-
}
279-
}
280-
281243
/// The possible types of `MongoCursor` or `SyncMongoCursor` an operation can return.
282244
public enum CursorType {
283245
/**

Sources/MongoSwift/Operations/CountOperation.swift renamed to Sources/MongoSwift/Operations/CountDocumentsOperation.swift

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import mongoc
22

3-
/// Options to use when executing a `count` command on a `MongoCollection` or a `SyncMongoCollection`.
4-
public struct CountOptions: Codable {
3+
/// Options to use when executing a `countDocuments` command on a `MongoCollection`.
4+
public struct CountDocumentsOptions: Codable {
55
/// Specifies a collation.
66
public var collation: Document?
77

@@ -50,12 +50,12 @@ public struct CountOptions: Codable {
5050
}
5151

5252
/// An operation corresponding to a "count" command on a collection.
53-
internal struct CountOperation<T: Codable>: Operation {
53+
internal struct CountDocumentsOperation<T: Codable>: Operation {
5454
private let collection: SyncMongoCollection<T>
5555
private let filter: Document
56-
private let options: CountOptions?
56+
private let options: CountDocumentsOptions?
5757

58-
internal init(collection: SyncMongoCollection<T>, filter: Document, options: CountOptions?) {
58+
internal init(collection: SyncMongoCollection<T>, filter: Document, options: CountDocumentsOptions?) {
5959
self.collection = collection
6060
self.filter = filter
6161
self.options = options
@@ -66,18 +66,7 @@ internal struct CountOperation<T: Codable>: Operation {
6666
let rp = self.options?.readPreference?._readPreference
6767
var error = bson_error_t()
6868
let count = self.collection.withMongocCollection(from: connection) { collPtr in
69-
// because we already encode skip and limit in the options,
70-
// pass in 0s so we don't get duplicate parameter errors.
71-
mongoc_collection_count_with_opts(
72-
collPtr,
73-
MONGOC_QUERY_NONE,
74-
self.filter._bson,
75-
0, // skip
76-
0, // limit
77-
opts?._bson,
78-
rp,
79-
&error
80-
)
69+
mongoc_collection_count_documents(collPtr, self.filter._bson, opts?._bson, rp, nil, &error)
8170
}
8271

8372
guard count != -1 else { throw extractMongoError(error: error) }
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import mongoc
2+
3+
/// Options to use when executing an `estimatedDocumentCount` command on a `MongoCollection`.
4+
public struct EstimatedDocumentCountOptions: Codable {
5+
/// The maximum amount of time to allow the query to run.
6+
public var maxTimeMS: Int64?
7+
8+
/// A ReadConcern to use for this operation.
9+
public var readConcern: ReadConcern?
10+
11+
// swiftlint:disable redundant_optional_initialization
12+
/// A ReadPreference to use for this operation.
13+
public var readPreference: ReadPreference? = nil
14+
// swiftlint:enable redundant_optional_initialization
15+
16+
/// Convenience initializer allowing any/all parameters to be optional
17+
public init(
18+
maxTimeMS: Int64? = nil,
19+
readConcern: ReadConcern? = nil,
20+
readPreference: ReadPreference? = nil
21+
) {
22+
self.maxTimeMS = maxTimeMS
23+
self.readConcern = readConcern
24+
self.readPreference = readPreference
25+
}
26+
27+
private enum CodingKeys: String, CodingKey {
28+
case maxTimeMS, readConcern
29+
}
30+
}
31+
32+
/// An operation corresponding to a "count" command on a collection.
33+
internal struct EstimatedDocumentCountOperation<T: Codable>: Operation {
34+
private let collection: SyncMongoCollection<T>
35+
private let options: EstimatedDocumentCountOptions?
36+
37+
internal init(collection: SyncMongoCollection<T>, options: EstimatedDocumentCountOptions?) {
38+
self.collection = collection
39+
self.options = options
40+
}
41+
42+
internal func execute(using connection: Connection, session: SyncClientSession?) throws -> Int {
43+
let opts = try encodeOptions(options: options, session: session)
44+
let rp = self.options?.readPreference?._readPreference
45+
var error = bson_error_t()
46+
let count = self.collection.withMongocCollection(from: connection) { collPtr in
47+
mongoc_collection_estimated_document_count(collPtr, opts?._bson, rp, nil, &error)
48+
}
49+
50+
guard count != -1 else { throw extractMongoError(error: error) }
51+
52+
return Int(count)
53+
}
54+
}

Tests/MongoSwiftTests/ClientSessionTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ final class ClientSessionTests: MongoSwiftTestCase {
3535
(name: "find", body: { _ = try $0.find([:], session: $1).nextOrError() }),
3636
(name: "aggregate", body: { _ = try $0.aggregate([], session: $1).nextOrError() }),
3737
(name: "distinct", body: { _ = try $0.distinct(fieldName: "x", session: $1) }),
38-
(name: "count", body: { _ = try $0.count(session: $1) })
38+
(name: "countDocuments", body: { _ = try $0.countDocuments(session: $1) }),
39+
(name: "estimatedDocumentCount", body: { _ = try $0.estimatedDocumentCount(session: $1) })
3940
]
4041

4142
// list of write operations on SyncMongoCollection that take in a session

Tests/MongoSwiftTests/CodecTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@ final class CodecTests: MongoSwiftTestCase {
805805
"writeConcern"
806806
]))
807807

808-
let count = CountOptions(
808+
let count = CountDocumentsOptions(
809809
collation: Document(),
810810
hint: .indexName("hint"),
811811
limit: 123,

Tests/MongoSwiftTests/CommandMonitoringTests.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ final class CommandMonitoringTests: MongoSwiftTestCase {
2222
// read in the file data and parse into a struct
2323
let name = filename.components(separatedBy: ".")[0]
2424

25-
// remove this if/when bulkwrite is supported
25+
// TODO: SWIFT-346: remove this skip
2626
if name.lowercased().contains("bulkwrite") { continue }
2727

28+
// remove this when command.json is updated with the new count API (see SPEC-1272)
29+
if name.lowercased() == "command" { continue }
30+
2831
print("-----------------------")
2932
print("Executing tests for file \(name)...\n")
3033

@@ -149,7 +152,7 @@ private struct CMTest: Decodable {
149152

150153
switch self.op.name {
151154
case "count":
152-
_ = try? collection.count(filter)
155+
_ = try? collection.countDocuments(filter)
153156
case "deleteMany":
154157
_ = try? collection.deleteMany(filter)
155158
case "deleteOne":

Tests/MongoSwiftTests/CrudTests.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ final class CrudTests: MongoSwiftTestCase {
3030
print("\n------------\nExecuting tests from file \(dir)/\(filename)...\n")
3131

3232
// For each file, execute the test cases contained in it
33-
for (i, test) in file.tests.enumerated() {
33+
for (i, test) in try file.makeTests().enumerated() {
34+
if type(of: test) == CountTest.self {
35+
print("Skipping test for old count API, no longer supported by the driver")
36+
}
37+
3438
print("Executing test: \(test.description)")
3539

3640
// for each test case:
@@ -40,10 +44,18 @@ final class CrudTests: MongoSwiftTestCase {
4044
// 4) verify that expected data is present
4145
// 5) drop the collection to clean up
4246
let collection = db.collection(self.getCollectionName(suffix: "\(filename)_\(i)"))
43-
try collection.insertMany(file.data)
47+
if !file.data.isEmpty {
48+
try collection.insertMany(file.data)
49+
}
4450
try test.execute(usingCollection: collection)
4551
try test.verifyData(testCollection: collection, db: db)
46-
try collection.drop()
52+
do {
53+
try collection.drop()
54+
} catch let ServerError.commandError(code, _, _, _) where code == 26 {
55+
// ignore ns not found errors
56+
} catch {
57+
throw error
58+
}
4759
}
4860
}
4961
print() // for readability of results
@@ -64,7 +76,11 @@ final class CrudTests: MongoSwiftTestCase {
6476
private struct CrudTestFile: Decodable {
6577
let data: [Document]
6678
let testDocs: [Document]
67-
var tests: [CrudTest] { return try! self.testDocs.map { try makeCrudTest($0) } }
79+
80+
func makeTests() throws -> [CrudTest] {
81+
return try self.testDocs.map { try makeCrudTest($0) }
82+
}
83+
6884
let minServerVersion: String?
6985
let maxServerVersion: String?
7086

@@ -88,9 +104,11 @@ private var testTypeMap: [String: CrudTest.Type] = [
88104
"aggregate": AggregateTest.self,
89105
"bulkWrite": BulkWriteTest.self,
90106
"count": CountTest.self,
107+
"countDocuments": CountDocumentsTest.self,
91108
"deleteMany": DeleteTest.self,
92109
"deleteOne": DeleteTest.self,
93110
"distinct": DistinctTest.self,
111+
"estimatedDocumentCount": EstimatedDocumentCountTest.self,
94112
"find": FindTest.self,
95113
"findOneAndDelete": FindOneAndDeleteTest.self,
96114
"findOneAndUpdate": FindOneAndUpdateTest.self,
@@ -270,10 +288,24 @@ private class BulkWriteTest: CrudTest {
270288

271289
/// A class for executing `count` tests
272290
private class CountTest: CrudTest {
291+
override func execute(usingCollection _: SyncMongoCollection<Document>) throws {}
292+
}
293+
294+
/// A class for executing `countDocuments` tests
295+
private class CountDocumentsTest: CrudTest {
273296
override func execute(usingCollection coll: SyncMongoCollection<Document>) throws {
274297
let filter: Document = try self.args.get("filter")
275-
let options = CountOptions(collation: self.collation, limit: self.limit, skip: self.skip)
276-
let result = try coll.count(filter, options: options)
298+
let options = CountDocumentsOptions(collation: self.collation, limit: self.limit, skip: self.skip)
299+
let result = try coll.countDocuments(filter, options: options)
300+
expect(result).to(equal(self.result?.asInt()))
301+
}
302+
}
303+
304+
/// A class for executing `estimatedDocumentCount` tests
305+
private class EstimatedDocumentCountTest: CrudTest {
306+
override func execute(usingCollection coll: SyncMongoCollection<Document>) throws {
307+
let options = EstimatedDocumentCountOptions()
308+
let result = try coll.estimatedDocumentCount(options: options)
277309
expect(result).to(equal(self.result?.asInt()))
278310
}
279311
}

0 commit comments

Comments
 (0)