1 /**
2 	MongoCollection class
3 
4 	Copyright: © 2012-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.db.mongo.collection;
9 
10 public import vibe.db.mongo.cursor;
11 public import vibe.db.mongo.connection;
12 public import vibe.db.mongo.flags;
13 
14 public import vibe.db.mongo.impl.index;
15 public import vibe.db.mongo.impl.crud;
16 
17 import vibe.core.log;
18 import vibe.db.mongo.client;
19 
20 import core.time;
21 import std.algorithm : among, countUntil, find, findSplit;
22 import std.array;
23 import std.conv;
24 import std.exception;
25 import std.meta : AliasSeq;
26 import std.string;
27 import std.typecons : Nullable, tuple, Tuple;
28 
29 
30 /**
31   Represents a single collection inside a MongoDB.
32 
33   All methods take arbitrary types for Bson arguments. serializeToBson() is implicitly called on
34   them before they are send to the database. The following example shows some possible ways
35   to specify objects.
36  */
37 struct MongoCollection {
38 	private {
39 		MongoClient m_client;
40 		MongoDatabase m_db;
41 		string m_name;
42 		string m_fullPath;
43 	}
44 
45 	this(MongoClient client, string fullPath)
46 	@safe {
47 		assert(client !is null);
48 		m_client = client;
49 
50 		auto dotidx = fullPath.indexOf('.');
51 		assert(dotidx > 0, "The collection name passed to MongoCollection must be of the form \"dbname.collectionname\".");
52 
53 		m_fullPath = fullPath;
54 		m_db = m_client.getDatabase(fullPath[0 .. dotidx]);
55 		m_name = fullPath[dotidx+1 .. $];
56 	}
57 
58 	this(ref MongoDatabase db, string name)
59 	@safe {
60 		assert(db.client !is null);
61 		m_client = db.client;
62 		m_fullPath = db.name ~ "." ~ name;
63 		m_db = db;
64 		m_name = name;
65 	}
66 
67 	/**
68 	  Returns: Root database to which this collection belongs.
69 	 */
70 	@property MongoDatabase database() @safe { return m_db; }
71 
72 	/**
73 	  Returns: Name of this collection (excluding the database name).
74 	 */
75 	@property string name() const @safe { return m_name; }
76 
77 	/**
78 	  Performs an update operation on documents matching 'selector', updating them with 'update'.
79 
80 	  Throws: Exception if a DB communication error occurred.
81 	  See_Also: $(LINK http://www.mongodb.org/display/DOCS/Updating)
82 	 */
83 	deprecated("Use `replaceOne`, `updateOne` or `updateMany` taking `UpdateOptions` instead, this method breaks in MongoDB 5.1 and onwards.")
84 	void update(T, U)(T selector, U update, UpdateFlags flags = UpdateFlags.None)
85 	{
86 		assert(m_client !is null, "Updating uninitialized MongoCollection.");
87 		auto conn = m_client.lockConnection();
88 		ubyte[256] selector_buf = void, update_buf = void;
89 		conn.update(m_fullPath, flags, serializeToBson(selector, selector_buf), serializeToBson(update, update_buf));
90 	}
91 
92 	/**
93 	  Inserts new documents into the collection.
94 
95 	  Note that if the `_id` field of the document(s) is not set, typically
96 	  using `BsonObjectID.generate()`, the server will generate IDs
97 	  automatically. If you need to know the IDs of the inserted documents,
98 	  you need to generate them locally.
99 
100 	  Throws: Exception if a DB communication error occurred.
101 	  See_Also: $(LINK http://www.mongodb.org/display/DOCS/Inserting)
102 	 */
103 	deprecated("Use `insertOne` or `insertMany`, this method breaks in MongoDB 5.1 and onwards.")
104 	void insert(T)(T document_or_documents, InsertFlags flags = InsertFlags.None)
105 	{
106 		assert(m_client !is null, "Inserting into uninitialized MongoCollection.");
107 		auto conn = m_client.lockConnection();
108 		Bson[] docs;
109 		Bson bdocs = () @trusted { return serializeToBson(document_or_documents); } ();
110 		if( bdocs.type == Bson.Type.Array ) docs = cast(Bson[])bdocs;
111 		else docs = () @trusted { return (&bdocs)[0 .. 1]; } ();
112 		conn.insert(m_fullPath, flags, docs);
113 	}
114 
115 	/**
116 		Inserts the provided document(s). If a document is missing an identifier,
117 		one is generated automatically by vibe.d.
118 
119 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/#mongodb-method-db.collection.insertOne)
120 
121 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/insert/)
122 	*/
123 	InsertOneResult insertOne(T)(T document, InsertOneOptions options = InsertOneOptions.init)
124 	{
125 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
126 
127 		Bson cmd = Bson.emptyObject; // empty object because order is important
128 		cmd["insert"] = Bson(m_name);
129 		auto doc = serializeToBson(document);
130 		enforce(doc.type == Bson.Type.object, "Can only insert objects into collections");
131 		InsertOneResult res;
132 		if ("_id" !in doc.get!(Bson[string]))
133 		{
134 			doc["_id"] = Bson(res.insertedId = BsonObjectID.generate);
135 		}
136 		cmd["documents"] = Bson([doc]);
137 		MongoConnection conn = m_client.lockConnection();
138 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
139 		foreach (string k, v; serializeToBson(options).byKeyValue)
140 			cmd[k] = v;
141 		
142 		database.runCommandChecked(cmd);
143 		return res;
144 	}
145 
146 	/// ditto
147 	InsertOneResult insertMany(T)(T[] documents, InsertManyOptions options = InsertManyOptions.init)
148 	{
149 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
150 
151 		Bson cmd = Bson.emptyObject; // empty object because order is important
152 		cmd["insert"] = Bson(m_name);
153 		Bson[] arr = new Bson[documents.length];
154 		BsonObjectID[size_t] insertedIds;
155 		foreach (i, document; documents)
156 		{
157 			auto doc = serializeToBson(document);
158 			arr[i] = doc;
159 			enforce(doc.type == Bson.Type.object, "Can only insert objects into collections");
160 			if ("_id" !in doc.get!(Bson[string]))
161 			{
162 				doc["_id"] = Bson(insertedIds[i] = BsonObjectID.generate);
163 			}
164 		}
165 		cmd["documents"] = Bson(arr);
166 		MongoConnection conn = m_client.lockConnection();
167 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
168 		foreach (string k, v; serializeToBson(options).byKeyValue)
169 			cmd[k] = v;
170 		
171 		database.runCommandChecked(cmd);
172 		return InsertManyResult(insertedIds);
173 	}
174 
175 	/**
176 		Deletes at most one document matching the query `filter`. The returned
177 		result identifies how many documents have been deleted.
178 
179 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.deleteOne/#mongodb-method-db.collection.deleteOne)
180 
181 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/delete/)
182 	*/
183 	DeleteResult deleteOne(T)(T filter, DeleteOptions options = DeleteOptions.init)
184 	@trusted {
185 		int limit = 1;
186 		return deleteImpl([filter], options, (&limit)[0 .. 1]);
187 	}
188 
189 	/**
190 		Deletes all documents matching the query `filter`. The returned result
191 		identifies how many documents have been deleted.
192 
193 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.deleteMany/#mongodb-method-db.collection.deleteMany)
194 
195 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/delete/)
196 	*/
197 	DeleteResult deleteMany(T)(T filter, DeleteOptions options = DeleteOptions.init)
198 	@safe
199 	if (!is(T == DeleteOptions))
200 	{
201 		return deleteImpl([filter], options);
202 	}
203 
204 	/**
205 		Deletes all documents in the collection. The returned result identifies
206 		how many documents have been deleted.
207 
208 		Same as calling `deleteMany` with `Bson.emptyObject` as filter.
209 
210 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/delete/)
211 	*/
212 	DeleteResult deleteAll(DeleteOptions options = DeleteOptions.init)
213 	@safe {
214 		return deleteImpl([Bson.emptyObject], options);
215 	}
216 
217 	/// Implementation helper. It's possible to set custom delete limits with
218 	/// this method, otherwise it's identical to `deleteOne` and `deleteMany`.
219 	DeleteResult deleteImpl(T)(T[] queries, DeleteOptions options = DeleteOptions.init, scope int[] limits = null)
220 	@safe {
221 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
222 
223 		alias FieldsMovedIntoChildren = AliasSeq!("limit", "collation", "hint");
224 
225 		Bson cmd = Bson.emptyObject; // empty object because order is important
226 		cmd["delete"] = Bson(m_name);
227 
228 		MongoConnection conn = m_client.lockConnection();
229 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
230 		auto optionsBson = serializeToBson(options);
231 		foreach (string k, v; optionsBson.byKeyValue)
232 			if (!k.among!FieldsMovedIntoChildren)
233 				cmd[k] = v;
234 
235 		Bson[] deletesBson = new Bson[queries.length];
236 		foreach (i, q; queries)
237 		{
238 			auto deleteBson = Bson.emptyObject;
239 			deleteBson["q"] = serializeToBson(q);
240 			foreach (string k, v; optionsBson.byKeyValue)
241 				if (k.among!FieldsMovedIntoChildren)
242 					deleteBson[k] = v;
243 			if (i < limits.length)
244 				deleteBson["limit"] = Bson(limits[i]);
245 			else
246 				deleteBson["limit"] = Bson(0);
247 			deletesBson[i] = deleteBson;
248 		}
249 		cmd["deletes"] = Bson(deletesBson);
250 
251 		auto n = database.runCommandChecked(cmd)["n"].to!long;
252 		return DeleteResult(n);
253 	}
254 
255 	/**
256 		Replaces at most single document within the collection based on the filter.
257 
258 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.replaceOne/#mongodb-method-db.collection.replaceOne)
259 
260 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/update/)
261 	*/
262 	UpdateResult replaceOne(T, U)(T filter, U replacement, UpdateOptions options = UpdateOptions.init)
263 	@safe {
264 		Bson opts = Bson.emptyObject;
265 		opts["multi"] = Bson(false);
266 		return updateImpl([filter], [replacement], [opts], options, true, false);
267 	}
268 
269 	/**
270 		Updates at most single document within the collection based on the filter.
271 
272 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.updateOne/#mongodb-method-db.collection.updateOne)
273 
274 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/update/)
275 	*/
276 	UpdateResult updateOne(T, U)(T filter, U replacement, UpdateOptions options = UpdateOptions.init)
277 	@safe {
278 		Bson opts = Bson.emptyObject;
279 		opts["multi"] = Bson(false);
280 		return updateImpl([filter], [replacement], [opts], options, false, true);
281 	}
282 
283 	/**
284 		Updates all matching document within the collection based on the filter.
285 
286 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.updateMany/#mongodb-method-db.collection.updateMany)
287 
288 		Standards: $(LINK https://www.mongodb.com/docs/manual/reference/command/update/)
289 	*/
290 	UpdateResult updateMany(T, U)(T filter, U replacement, UpdateOptions options = UpdateOptions.init)
291 	@safe {
292 		Bson opts = Bson.emptyObject;
293 		opts["multi"] = Bson(true);
294 		return updateImpl([filter], [replacement], [opts], options, false, true);
295 	}
296 
297 	/// Implementation helper. It's possible to set custom per-update object
298 	/// options with this method, otherwise it's identical to `replaceOne`,
299 	/// `updateOne` and `updateMany`.
300 	UpdateResult updateImpl(T, U, O)(T[] queries, U[] documents, O[] perUpdateOptions, UpdateOptions options = UpdateOptions.init,
301 		bool mustBeDocument = false, bool mustBeModification = false)
302 	@safe
303 	in(queries.length == documents.length && documents.length == perUpdateOptions.length,
304 		"queries, documents and perUpdateOptions must have same length")
305 	{
306 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
307 
308 		alias FieldsMovedIntoChildren = AliasSeq!("arrayFilters",
309 			"collation",
310 			"hint",
311 			"upsert");
312 
313 		Bson cmd = Bson.emptyObject; // empty object because order is important
314 		cmd["update"] = Bson(m_name);
315 
316 		MongoConnection conn = m_client.lockConnection();
317 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
318 		auto optionsBson = serializeToBson(options);
319 		foreach (string k, v; optionsBson.byKeyValue)
320 			if (!k.among!FieldsMovedIntoChildren)
321 				cmd[k] = v;
322 
323 		Bson[] updatesBson = new Bson[queries.length];
324 		foreach (i, q; queries)
325 		{
326 			auto updateBson = Bson.emptyObject;
327 			auto qbson = serializeToBson(q);
328 			updateBson["q"] = qbson;
329 			if (mustBeDocument)
330 			{
331 				if (qbson.type != Bson.Type.object)
332 					assert(false, "Passed in non-document into a place where only replacements are expected. "
333 						~ "Maybe you want to call updateOne or updateMany instead?");
334 
335 				foreach (string k, v; qbson.byKeyValue)
336 				{
337 					if (k.startsWith("$"))
338 						assert(false, "Passed in atomic modifiers (" ~ k
339 							~ ") into a place where only replacements are expected. "
340 							~ "Maybe you want to call updateOne or updateMany instead?");
341 					debug break; // server checks that the rest is consistent (only $ or only non-$ allowed)
342 					// however in debug mode we check the full document, as we can give better error messages to the dev
343 				}
344 			}
345 			if (mustBeModification)
346 			{
347 				if (qbson.type == Bson.Type.object)
348 				{
349 					bool anyDollar = false;
350 					foreach (string k, v; qbson.byKeyValue)
351 					{
352 						if (k.startsWith("$"))
353 							anyDollar = true;
354 						debug break; // server checks that the rest is consistent (only $ or only non-$ allowed)
355 						// however in debug mode we check the full document, as we can give better error messages to the dev
356 						// also nice side effect: if this is an empty document, this also matches the assert(false) branch.
357 					}
358 
359 					if (!anyDollar)
360 						assert(false, "Passed in a regular document into a place where only updates are expected. "
361 							~ "Maybe you want to call replaceOne instead? "
362 							~ "(this update call would otherwise replace the entire matched object with the passed in update object)");
363 				}
364 			}
365 			updateBson["u"] = serializeToBson(documents[i]);
366 			foreach (string k, v; optionsBson.byKeyValue)
367 				if (k.among!FieldsMovedIntoChildren)
368 					updateBson[k] = v;
369 			foreach (string k, v; perUpdateOptions[i].byKeyValue)
370 				updateBson[k] = v;
371 			updatesBson[i] = updateBson;
372 		}
373 		cmd["updates"] = Bson(updatesBson);
374 
375 		auto res = database.runCommandChecked(cmd);
376 		auto ret = UpdateResult(
377 			res["n"].to!long,
378 			res["nModified"].to!long,
379 		);
380 		auto upserted = res["upserted"].opt!(Bson[]);
381 		if (upserted.length)
382 		{
383 			ret.upsertedIds.length = upserted.length;
384 			foreach (i, id; upserted)
385 				ret.upsertedIds[i] = id.get!BsonObjectID;
386 		}
387 		return ret;
388 	}
389 
390 	deprecated("Use the overload taking `FindOptions` instead, this method breaks in MongoDB 5.1 and onwards. Note: using a `$query` / `query` member to override the query arguments is no longer supported in the new overload.")
391 	MongoCursor!R find(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags, int num_skip = 0, int num_docs_per_chunk = 0)
392 	{
393 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
394 		return MongoCursor!R(m_client, m_fullPath, flags, num_skip, num_docs_per_chunk, query, returnFieldSelector);
395 	}
396 
397 	///
398 	@safe deprecated unittest {
399 		import vibe.db.mongo.mongo;
400 
401 		void test()
402 		{
403 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
404 			// find documents with status == "A"
405 			auto x = coll.find(["status": "A"], ["status": true], QueryFlags.none);
406 			foreach (item; x)
407 			{
408 				// only for legacy overload
409 			}
410 		}
411 	}
412 
413 	/**
414 	  Queries the collection for existing documents, limiting what fields are
415 	  returned by the database. (called projection)
416 
417 	  See_Also:
418 	  - Querying: $(LINK http://www.mongodb.org/display/DOCS/Querying)
419 	  - Projection: $(LINK https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#std-label-projections)
420 	  - $(LREF findOne)
421 	 */
422 	MongoCursor!R find(R = Bson, T, U)(T query, U projection, FindOptions options = FindOptions.init)
423 	if (!is(U == FindOptions))
424 	{
425 		options.projection = serializeToBson(projection);
426 		return find(query, options);
427 	}
428 
429 	///
430 	@safe unittest {
431 		import vibe.db.mongo.mongo;
432 
433 		void test()
434 		{
435 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
436 			// find documents with status == "A", return list of {"item":..., "status":...}
437 			coll.find(["status": "A"], ["item": 1, "status": 1]);
438 		}
439 	}
440 
441 	/**
442 	  Queries the collection for existing documents.
443 
444 	  If no arguments are passed to find(), all documents of the collection will be returned.
445 
446 	  See_Also:
447 	  - $(LINK http://www.mongodb.org/display/DOCS/Querying)
448 	  - $(LREF findOne)
449 	 */
450 	MongoCursor!R find(R = Bson, Q)(Q query, FindOptions options = FindOptions.init)
451 	{
452 		return MongoCursor!R(m_client, m_db.name, m_name, query, options);
453 	}
454 
455 	///
456 	@safe unittest {
457 		import vibe.db.mongo.mongo;
458 
459 		void test()
460 		{
461 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
462 			// find documents with status == "A"
463 			coll.find(["status": "A"]);
464 		}
465 	}
466 
467 	/**
468 	  Queries all documents of the collection.
469 
470 	  See_Also:
471 	  - $(LINK http://www.mongodb.org/display/DOCS/Querying)
472 	  - $(LREF findOne)
473 	 */
474 	MongoCursor!R find(R = Bson)() { return find!R(Bson.emptyObject, FindOptions.init); }
475 
476 	///
477 	@safe unittest {
478 		import vibe.db.mongo.mongo;
479 
480 		void test()
481 		{
482 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
483 			// find all documents in the "test" collection.
484 			coll.find();
485 		}
486 	}
487 
488 	deprecated("Use the overload taking `FindOptions` instead, this method breaks in MongoDB 5.1 and onwards. Note: using a `$query` / `query` member to override the query arguments is no longer supported in the new overload.")
489 	auto findOne(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags)
490 	{
491 		import std.traits;
492 		import std.typecons;
493 
494 		auto c = find!R(query, returnFieldSelector, flags, 0, 1);
495 		static if (is(R == Bson)) {
496 			foreach (doc; c) return doc;
497 			return Bson(null);
498 		} else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) {
499 			foreach (doc; c) return doc;
500 			return null;
501 		} else {
502 			foreach (doc; c) {
503 				Nullable!R ret;
504 				ret = doc;
505 				return ret;
506 			}
507 			return Nullable!R.init;
508 		}
509 	}
510 
511 	/** Queries the collection for existing documents.
512 
513 		Returns:
514 			By default, a Bson value of the matching document is returned, or $(D Bson(null))
515 			when no document matched. For types R that are not Bson, the returned value is either
516 			of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type.
517 
518 			The projection parameter limits what fields are returned by the database,
519 			see projection documentation linked below.
520 
521 		Throws: Exception if a DB communication error or a query error occurred.
522 
523 		See_Also:
524 		- Querying: $(LINK http://www.mongodb.org/display/DOCS/Querying)
525 		- Projection: $(LINK https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#std-label-projections)
526 		- $(LREF find)
527 	 */
528 	auto findOne(R = Bson, T, U)(T query, U projection, FindOptions options = FindOptions.init)
529 	if (!is(U == FindOptions))
530 	{
531 		options.projection = serializeToBson(projection);
532 		return findOne!(R, T)(query, options);
533 	}
534 
535 	///
536 	@safe unittest {
537 		import vibe.db.mongo.mongo;
538 
539 		void test()
540 		{
541 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
542 			// find documents with status == "A"
543 			auto x = coll.findOne(["status": "A"], ["status": true, "otherField": true]);
544 			// x now only contains _id (implicit, unless you make it `false`), status and otherField
545 		}
546 	}
547 
548 	/** Queries the collection for existing documents.
549 
550 		Returns:
551 			By default, a Bson value of the matching document is returned, or $(D Bson(null))
552 			when no document matched. For types R that are not Bson, the returned value is either
553 			of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type.
554 
555 		Throws: Exception if a DB communication error or a query error occurred.
556 		See_Also:
557 		- $(LINK http://www.mongodb.org/display/DOCS/Querying)
558 		- $(LREF find)
559 	 */
560 	auto findOne(R = Bson, T)(T query, FindOptions options = FindOptions.init)
561 	{
562 		import std.traits;
563 		import std.typecons;
564 
565 		options.limit = 1;
566 		auto c = find!R(query, options);
567 		static if (is(R == Bson)) {
568 			foreach (doc; c) return doc;
569 			return Bson(null);
570 		} else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) {
571 			foreach (doc; c) return doc;
572 			return null;
573 		} else {
574 			foreach (doc; c) {
575 				Nullable!R ret;
576 				ret = doc;
577 				return ret;
578 			}
579 			return Nullable!R.init;
580 		}
581 	}
582 
583 	/**
584 	  Removes documents from the collection.
585 
586 	  Throws: Exception if a DB communication error occurred.
587 	  See_Also: $(LINK http://www.mongodb.org/display/DOCS/Removing)
588 	 */
589 	deprecated("Use `deleteOne` or `deleteMany` taking DeleteOptions instead, this method breaks in MongoDB 5.1 and onwards.")
590 	void remove(T)(T selector, DeleteFlags flags = DeleteFlags.None)
591 	{
592 		assert(m_client !is null, "Removing from uninitialized MongoCollection.");
593 		auto conn = m_client.lockConnection();
594 		ubyte[256] selector_buf = void;
595 		conn.delete_(m_fullPath, flags, serializeToBson(selector, selector_buf));
596 	}
597 
598 	/// ditto
599 	deprecated("Use `deleteMany` taking `DeleteOptions` instead, this method breaks in MongoDB 5.1 and onwards.")
600 	void remove()() { remove(Bson.emptyObject); }
601 
602 	/**
603 		Combines a modify and find operation to a single atomic operation.
604 
605 		Params:
606 			query = MongoDB query expression to identify the matched document
607 			update = Update expression for the matched document
608 			returnFieldSelector = Optional map of fields to return in the response
609 
610 		Throws:
611 			An `Exception` will be thrown if an error occurs in the
612 			communication with the database server.
613 
614 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify)
615 	 */
616 	Bson findAndModify(T, U, V)(T query, U update, V returnFieldSelector)
617 	{
618 		static struct CMD {
619 			string findAndModify;
620 			T query;
621 			U update;
622 			V fields;
623 		}
624 		CMD cmd;
625 		cmd.findAndModify = m_name;
626 		cmd.query = query;
627 		cmd.update = update;
628 		cmd.fields = returnFieldSelector;
629 		auto ret = database.runCommandChecked(cmd);
630 		return ret["value"];
631 	}
632 
633 	/// ditto
634 	Bson findAndModify(T, U)(T query, U update)
635 	{
636 		return findAndModify(query, update, null);
637 	}
638 
639 	/**
640 		Combines a modify and find operation to a single atomic operation with generic options support.
641 
642 		Params:
643 			query = MongoDB query expression to identify the matched document
644 			update = Update expression for the matched document
645 			options = Generic BSON object that contains additional options
646 				fields, such as `"new": true`
647 
648 		Throws:
649 			An `Exception` will be thrown if an error occurs in the
650 			communication with the database server.
651 
652 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify)
653 	 */
654 	Bson findAndModifyExt(T, U, V)(T query, U update, V options)
655 	{
656 		auto bopt = serializeToBson(options);
657 		assert(bopt.type == Bson.Type.object,
658 			"The options parameter to findAndModifyExt must be a BSON object.");
659 
660 		Bson cmd = Bson.emptyObject;
661 		cmd["findAndModify"] = m_name;
662 		cmd["query"] = serializeToBson(query);
663 		cmd["update"] = serializeToBson(update);
664 		bopt.opApply(delegate int(string key, Bson value) @safe {
665 			cmd[key] = value;
666 			return 0;
667 		});
668 		auto ret = database.runCommandChecked(cmd);
669 		return ret["value"];
670 	}
671 
672 	///
673 	@safe unittest {
674 		import vibe.db.mongo.mongo;
675 
676 		void test()
677 		{
678 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
679 			coll.findAndModifyExt(["name": "foo"], ["$set": ["value": "bar"]], ["new": true]);
680 		}
681 	}
682 
683 	deprecated("deprecated since MongoDB v4.0, use countDocuments or estimatedDocumentCount instead")
684 	ulong count(T)(T query)
685 	{
686 		return countImpl!T(query);
687 	}
688 
689 	private ulong countImpl(T)(T query)
690 	{
691 		Bson cmd = Bson.emptyObject;
692 		cmd["count"] = m_name;
693 		cmd["query"] = serializeToBson(query);
694 		auto reply = database.runCommandChecked(cmd);
695 		switch (reply["n"].type) with (Bson.Type) {
696 			default: assert(false, "Unsupported data type in BSON reply for COUNT");
697 			case double_: return cast(ulong)reply["n"].get!double; // v2.x
698 			case int_: return reply["n"].get!int; // v3.x
699 			case long_: return reply["n"].get!long; // just in case
700 		}
701 	}
702 
703 	/**
704 		Returns the count of documents that match the query for a collection or
705 		view.
706 		
707 		The method wraps the `$group` aggregation stage with a `$sum` expression
708 		to perform the count.
709 
710 		Throws Exception if a DB communication error occurred.
711 
712 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/)
713 	*/
714 	ulong countDocuments(T)(T filter, CountOptions options = CountOptions.init)
715 	{
716 		// https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details
717 		Bson[] pipeline = [Bson(["$match": serializeToBson(filter)])];
718 		if (!options.skip.isNull)
719 			pipeline ~= Bson(["$skip": Bson(options.skip.get)]);
720 		if (!options.limit.isNull)
721 			pipeline ~= Bson(["$limit": Bson(options.limit.get)]);
722 		pipeline ~= Bson(["$group": Bson([
723 			"_id": Bson(1),
724 			"n": Bson(["$sum": Bson(1)])
725 		])]);
726 		AggregateOptions aggOptions;
727 		foreach (i, field; options.tupleof)
728 		{
729 			enum name = CountOptions.tupleof[i].stringof;
730 			if (name != "filter" && name != "skip" && name != "limit")
731 				__traits(getMember, aggOptions, name) = field;
732 		}
733 		auto reply = aggregate(pipeline, aggOptions).front;
734 		return reply["n"].to!long;
735 	}
736 
737 	/**
738 		Returns the count of all documents in a collection or view.
739 
740 		Throws Exception if a DB communication error occurred.
741 
742 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.estimatedDocumentCount/)
743 	*/
744 	ulong estimatedDocumentCount(EstimatedDocumentCountOptions options = EstimatedDocumentCountOptions.init)
745 	{
746 		// https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details
747 		MongoConnection conn = m_client.lockConnection();
748 		if (conn.description.satisfiesVersion(WireVersion.v49)) {
749 			Bson[] pipeline = [
750 				Bson(["$collStats": Bson(["count": Bson.emptyObject])]),
751 				Bson(["$group": Bson([
752 					"_id": Bson(1),
753 					"n": Bson(["$sum": Bson("$count")])
754 				])])
755 			];
756 			AggregateOptions aggOptions;
757 			aggOptions.maxTimeMS = options.maxTimeMS;
758 			auto reply = aggregate(pipeline, aggOptions).front;
759 			return reply["n"].to!long;
760 		} else {
761 			return countImpl(null);
762 		}
763 	}
764 
765 	/**
766 		Calculates aggregate values for the data in a collection.
767 
768 		Params:
769 			pipeline = A sequence of data aggregation processes. These can
770 				either be given as separate parameters, or as a single array
771 				parameter.
772 
773 		Returns:
774 			Returns the list of documents aggregated by the pipeline. The return
775 			value is either a single `Bson` array value or a `MongoCursor`
776 			(input range) of the requested document type.
777 
778 		Throws: Exception if a DB communication error occurred.
779 
780 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/db.collection.aggregate)
781 	*/
782 	Bson aggregate(ARGS...)(ARGS pipeline) @safe
783 	{
784 		import std.traits : isArray;
785 
786 		static if (ARGS.length == 1 && isArray!(ARGS[0]))
787 			auto convPipeline = pipeline;
788 		else {
789 			static struct Pipeline { @asArray ARGS pipeline; }
790 
791 			Bson[] convPipeline = serializeToBson(Pipeline(pipeline))["pipeline"].get!(Bson[]);
792 		}
793 
794 		return aggregate(convPipeline, AggregateOptions.init).array.serializeToBson;
795 	}
796 
797 	/// ditto
798 	MongoCursor!R aggregate(R = Bson, S = Bson)(S[] pipeline, AggregateOptions options) @safe
799 	{
800 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
801 
802 		Bson cmd = Bson.emptyObject; // empty object because order is important
803 		cmd["aggregate"] = Bson(m_name);
804 		cmd["$db"] = Bson(m_db.name);
805 		cmd["pipeline"] = serializeToBson(pipeline);
806 		MongoConnection conn = m_client.lockConnection();
807 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
808 		foreach (string k, v; serializeToBson(options).byKeyValue)
809 		{
810 			// spec recommends to omit cursor field when explain is true
811 			if (!options.explain.isNull && options.explain.get && k == "cursor")
812 				continue;
813 			cmd[k] = v;
814 		}
815 		return MongoCursor!R(m_client, cmd,
816 			!options.batchSize.isNull ? options.batchSize.get : 0,
817 			!options.maxAwaitTimeMS.isNull ? options.maxAwaitTimeMS.get.msecs
818 				: !options.maxTimeMS.isNull ? options.maxTimeMS.get.msecs
819 				: Duration.max);
820 	}
821 
822 	/// Example taken from the MongoDB documentation
823 	@safe unittest {
824 		import vibe.db.mongo.mongo;
825 
826 		void test() {
827 			auto db = connectMongoDB("127.0.0.1").getDatabase("test");
828 			auto results = db["coll"].aggregate(
829 				["$match": ["status": "A"]],
830 				["$group": ["_id": Bson("$cust_id"),
831 					"total": Bson(["$sum": Bson("$amount")])]],
832 				["$sort": ["total": -1]]);
833 		}
834 	}
835 
836 	/// The same example, but using an array of arguments with custom options
837 	@safe unittest {
838 		import vibe.db.mongo.mongo;
839 
840 		void test() {
841 			auto db = connectMongoDB("127.0.0.1").getDatabase("test");
842 
843 			Bson[] args;
844 			args ~= serializeToBson(["$match": ["status": "A"]]);
845 			args ~= serializeToBson(["$group": ["_id": Bson("$cust_id"),
846 					"total": Bson(["$sum": Bson("$amount")])]]);
847 			args ~= serializeToBson(["$sort": ["total": -1]]);
848 
849 			AggregateOptions options;
850 			options.cursor.batchSize = 10; // pre-fetch the first 10 results
851 			auto results = db["coll"].aggregate(args, options);
852 		}
853 	}
854 
855 	/**
856 		Returns an input range of all unique values for a certain field for
857 		records matching the given query.
858 
859 		Params:
860 			fieldName = Name of the field for which to collect unique values
861 			query = The query used to select records
862 			options = Options to apply
863 
864 		Returns:
865 			An input range with items of type `R` (`Bson` by default) is
866 			returned.
867 	*/
868 	auto distinct(R = Bson, Q)(string fieldName, Q query, DistinctOptions options = DistinctOptions.init)
869 	{
870 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
871 
872 		Bson cmd = Bson.emptyObject; // empty object because order is important
873 		cmd["distinct"] = Bson(m_name);
874 		cmd["key"] = Bson(fieldName);
875 		cmd["query"] = serializeToBson(query);
876 		MongoConnection conn = m_client.lockConnection();
877 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
878 		foreach (string k, v; serializeToBson(options).byKeyValue)
879 			cmd[k] = v;
880 
881 		import std.algorithm : map;
882 
883 		auto res = m_db.runCommandChecked(cmd);
884 		static if (is(R == Bson)) return res["values"].byValue;
885 		else return res["values"].byValue.map!(b => deserializeBson!R(b));
886 	}
887 
888 	///
889 	@safe unittest {
890 		import std.algorithm : equal;
891 		import vibe.db.mongo.mongo;
892 
893 		void test()
894 		{
895 			auto db = connectMongoDB("127.0.0.1").getDatabase("test");
896 			auto coll = db["collection"];
897 
898 			coll.drop();
899 			coll.insertOne(["a": "first", "b": "foo"]);
900 			coll.insertOne(["a": "first", "b": "bar"]);
901 			coll.insertOne(["a": "first", "b": "bar"]);
902 			coll.insertOne(["a": "second", "b": "baz"]);
903 			coll.insertOne(["a": "second", "b": "bam"]);
904 
905 			auto result = coll.distinct!string("b", ["a": "first"]);
906 
907 			assert(result.equal(["foo", "bar"]));
908 		}
909 	}
910 
911 	/*
912 		following MongoDB standard API for the Index Management specification:
913 
914 		Standards: https://github.com/mongodb/specifications/blob/0c6e56141c867907aacf386e0cbe56d6562a0614/source/index-management.rst#standard-api
915 	*/
916 
917 	deprecated("This is a legacy API, call createIndexes instead")
918 	void ensureIndex(scope const(Tuple!(string, int))[] field_orders, IndexFlags flags = IndexFlags.none, Duration expire_time = 0.seconds)
919 	@safe {
920 		IndexModel[1] models;
921 		IndexOptions options;
922 		if (flags & IndexFlags.unique) options.unique = true;
923 		if (flags & IndexFlags.dropDuplicates) options.dropDups = true;
924 		if (flags & IndexFlags.background) options.background = true;
925 		if (flags & IndexFlags.sparse) options.sparse = true;
926 		if (flags & IndexFlags.expireAfterSeconds) options.expireAfter = expire_time;
927 
928 		models[0].options = options;
929 		foreach (field; field_orders) {
930 			models[0].add(field[0], field[1]);
931 		}
932 		createIndexes(models);
933 	}
934 
935 	deprecated("This is a legacy API, call createIndexes instead. This API is not recommended to be used because of unstable dictionary ordering.")
936 	void ensureIndex(int[string] field_orders, IndexFlags flags = IndexFlags.none, ulong expireAfterSeconds = 0)
937 	@safe {
938 		Tuple!(string, int)[] orders;
939 		foreach (k, v; field_orders)
940 			orders ~= tuple(k, v);
941 		ensureIndex(orders, flags, expireAfterSeconds.seconds);
942 	}
943 
944 	/**
945 		Drops a single index from the collection by the index name.
946 
947 		Throws: `Exception` if it is attempted to pass in `*`.
948 		Use dropIndexes() to remove all indexes instead.
949 	*/
950 	void dropIndex(string name, DropIndexOptions options = DropIndexOptions.init)
951 	@safe {
952 		if (name == "*")
953 			throw new Exception("Attempted to remove single index with '*'");
954 
955 		static struct CMD {
956 			string dropIndexes;
957 			string index;
958 		}
959 
960 		CMD cmd;
961 		cmd.dropIndexes = m_name;
962 		cmd.index = name;
963 		database.runCommandChecked(cmd);
964 	}
965 
966 	/// ditto
967 	void dropIndex(T)(T keys,
968 		IndexOptions indexOptions = IndexOptions.init,
969 		DropIndexOptions options = DropIndexOptions.init)
970 	@safe if (!is(Unqual!T == IndexModel))
971 	{
972 		IndexModel model;
973 		model.keys = serializeToBson(keys);
974 		model.options = indexOptions;
975 		dropIndex(model.name, options);
976 	}
977 
978 	/// ditto
979 	void dropIndex(const IndexModel keys,
980 		DropIndexOptions options = DropIndexOptions.init)
981 	@safe {
982 		dropIndex(keys.name, options);
983 	}
984 
985 	///
986 	@safe unittest
987 	{
988 		import vibe.db.mongo.mongo;
989 
990 		void test()
991 		{
992 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
993 			auto primarykey = IndexModel()
994 					.add("name", 1)
995 					.add("primarykey", -1);
996 			coll.dropIndex(primarykey);
997 		}
998 	}
999 
1000 	/// Drops all indexes in the collection.
1001 	void dropIndexes(DropIndexOptions options = DropIndexOptions.init)
1002 	@safe {
1003 		static struct CMD {
1004 			string dropIndexes;
1005 			string index;
1006 		}
1007 
1008 		CMD cmd;
1009 		cmd.dropIndexes = m_name;
1010 		cmd.index = "*";
1011 		database.runCommandChecked(cmd);
1012 	}
1013 
1014 	/// Unofficial API extension, more efficient multi-index removal on
1015 	/// MongoDB 4.2+
1016 	void dropIndexes(string[] names, DropIndexOptions options = DropIndexOptions.init)
1017 	@safe {
1018 		MongoConnection conn = m_client.lockConnection();
1019 		if (conn.description.satisfiesVersion(WireVersion.v42)) {
1020 			static struct CMD {
1021 				string dropIndexes;
1022 				string[] index;
1023 			}
1024 
1025 			CMD cmd;
1026 			cmd.dropIndexes = m_name;
1027 			cmd.index = names;
1028 			database.runCommandChecked(cmd);
1029 		} else {
1030 			foreach (name; names)
1031 				dropIndex(name);
1032 		}
1033 	}
1034 
1035 	///
1036 	@safe unittest
1037 	{
1038 		import vibe.db.mongo.mongo;
1039 
1040 		void test()
1041 		{
1042 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1043 			coll.dropIndexes(["name_1_primarykey_-1"]);
1044 		}
1045 	}
1046 
1047 	/**
1048 		Convenience method for creating a single index. Calls `createIndexes`
1049 
1050 		Supports any kind of document for template parameter T or a IndexModel.
1051 
1052 		Params:
1053 			keys = a IndexModel or type with integer or string fields indicating
1054 				index direction or index type.
1055 	*/
1056 	string createIndex(T)(T keys,
1057 		IndexOptions indexOptions = IndexOptions.init,
1058 		CreateIndexOptions options = CreateIndexOptions.init)
1059 	@safe if (!is(Unqual!T == IndexModel))
1060 	{
1061 		IndexModel[1] model;
1062 		model[0].keys = serializeToBson(keys);
1063 		model[0].options = indexOptions;
1064 		return createIndexes(model[], options)[0];
1065 	}
1066 
1067 	/// ditto
1068 	string createIndex(const IndexModel keys,
1069 		CreateIndexOptions options = CreateIndexOptions.init)
1070 	@safe {
1071 		IndexModel[1] model;
1072 		model[0] = keys;
1073 		return createIndexes(model[], options)[0];
1074 	}
1075 
1076 	///
1077 	@safe unittest
1078 	{
1079 		import vibe.db.mongo.mongo;
1080 
1081 		void test()
1082 		{
1083 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1084 
1085 			// simple ascending name, descending primarykey compound-index
1086 			coll.createIndex(["name": 1, "primarykey": -1]);
1087 
1088 			IndexOptions textOptions = {
1089 				// pick language from another field called "idioma"
1090 				languageOverride: "idioma"
1091 			};
1092 			auto textIndex = IndexModel()
1093 					.withOptions(textOptions)
1094 					.add("comments", IndexType.text);
1095 			// more complex text index in DB with independent language
1096 			coll.createIndex(textIndex);
1097 		}
1098 	}
1099 
1100 	/**
1101 		Builds one or more indexes in the collection.
1102 
1103 		See_Also: $(LINK https://docs.mongodb.com/manual/reference/command/createIndexes/)
1104 	*/
1105 	string[] createIndexes(scope const(IndexModel)[] models,
1106 		CreateIndexesOptions options = CreateIndexesOptions.init,
1107 		string file = __FILE__, size_t line = __LINE__)
1108 	@safe {
1109 		string[] keys = new string[models.length];
1110 
1111 		MongoConnection conn = m_client.lockConnection();
1112 		if (conn.description.satisfiesVersion(WireVersion.v26)) {
1113 			Bson cmd = Bson.emptyObject;
1114 			cmd["createIndexes"] = m_name;
1115 			Bson[] indexes;
1116 			foreach (model; models) {
1117 				// trusted to support old compilers which think opt_dup has
1118 				// longer lifetime than model.options
1119 				IndexOptions opt_dup = (() @trusted => model.options)();
1120 				enforceWireVersionConstraints(opt_dup, conn.description.maxWireVersion, file, line);
1121 				Bson index = serializeToBson(opt_dup);
1122 				index["key"] = model.keys;
1123 				index["name"] = model.name;
1124 				indexes ~= index;
1125 			}
1126 			cmd["indexes"] = Bson(indexes);
1127 			database.runCommandChecked(cmd);
1128 		} else {
1129 			foreach (model; models) {
1130 				// trusted to support old compilers which think opt_dup has
1131 				// longer lifetime than model.options
1132 				IndexOptions opt_dup = (() @trusted => model.options)();
1133 				enforceWireVersionConstraints(opt_dup, WireVersion.old, file, line);
1134 				Bson doc = serializeToBson(opt_dup);
1135 				doc["v"] = 1;
1136 				doc["key"] = model.keys;
1137 				doc["ns"] = m_fullPath;
1138 				doc["name"] = model.name;
1139 				database["system.indexes"].insertOne(doc);
1140 			}
1141 		}
1142 
1143 		return keys;
1144 	}
1145 
1146 	/**
1147 		Returns an array that holds a list of documents that identify and describe the existing indexes on the collection. 
1148 	*/
1149 	MongoCursor!R listIndexes(R = Bson)() 
1150 	@safe {
1151 		MongoConnection conn = m_client.lockConnection();
1152 		if (conn.description.satisfiesVersion(WireVersion.v30)) {
1153 			Bson command = Bson.emptyObject;
1154 			command["listIndexes"] = Bson(m_name);
1155 			command["$db"] = Bson(m_db.name);
1156 			return MongoCursor!R(m_client, command);
1157 		} else {
1158 			throw new MongoDriverException("listIndexes not supported on MongoDB <3.0");
1159 		}
1160 	}
1161 
1162 	///
1163 	@safe unittest
1164 	{
1165 		import vibe.db.mongo.mongo;
1166 
1167 		void test()
1168 		{
1169 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1170 
1171 			foreach (index; coll.listIndexes())
1172 				logInfo("index %s: %s", index["name"].get!string, index);
1173 		}
1174 	}
1175 
1176 	deprecated("Please use the standard API name 'listIndexes'") alias getIndexes = listIndexes;
1177 
1178 	/**
1179 		Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection.
1180 	*/
1181 	void drop()
1182 	@safe {
1183 		static struct CMD {
1184 			string drop;
1185 		}
1186 
1187 		CMD cmd;
1188 		cmd.drop = m_name;
1189 		database.runCommandChecked(cmd);
1190 	}
1191 }
1192 
1193 ///
1194 @safe unittest {
1195 	import vibe.data.bson;
1196 	import vibe.data.json;
1197 	import vibe.db.mongo.mongo;
1198 
1199 	void test()
1200 	{
1201 		MongoClient client = connectMongoDB("127.0.0.1");
1202 		MongoCollection users = client.getCollection("myapp.users");
1203 
1204 		// canonical version using a Bson object
1205 		users.insertOne(Bson(["name": Bson("admin"), "password": Bson("secret")]));
1206 
1207 		// short version using a string[string] AA that is automatically
1208 		// serialized to Bson
1209 		users.insertOne(["name": "admin", "password": "secret"]);
1210 
1211 		// BSON specific types are also serialized automatically
1212 		auto uid = BsonObjectID.fromString("507f1f77bcf86cd799439011");
1213 		Bson usr = users.findOne(["_id": uid]);
1214 
1215 		// JSON is another possibility
1216 		Json jusr = parseJsonString(`{"name": "admin", "password": "secret"}`);
1217 		users.insertOne(jusr);
1218 	}
1219 }
1220 
1221 /// Using the type system to define a document "schema"
1222 @safe unittest {
1223 	import vibe.db.mongo.mongo;
1224 	import vibe.data.serialization : name;
1225 	import std.typecons : Nullable;
1226 
1227 	// Nested object within a "User" document
1228 	struct Address {
1229 		string name;
1230 		string street;
1231 		int zipCode;
1232 	}
1233 
1234 	// The document structure of the "myapp.users" collection
1235 	struct User {
1236 		@name("_id") BsonObjectID id; // represented as "_id" in the database
1237 		string loginName;
1238 		string password;
1239 		Address address;
1240 	}
1241 
1242 	void test()
1243 	{
1244 		MongoClient client = connectMongoDB("127.0.0.1");
1245 		MongoCollection users = client.getCollection("myapp.users");
1246 
1247 		// D values are automatically serialized to the internal BSON format
1248 		// upon insertion - see also vibe.data.serialization
1249 		User usr;
1250 		usr.id = BsonObjectID.generate();
1251 		usr.loginName = "admin";
1252 		usr.password = "secret";
1253 		users.insertOne(usr);
1254 
1255 		// find supports direct de-serialization of the returned documents
1256 		foreach (usr2; users.find!User()) {
1257 			logInfo("User: %s", usr2.loginName);
1258 		}
1259 
1260 		// the same goes for findOne
1261 		Nullable!User qusr = users.findOne!User(["_id": usr.id]);
1262 		if (!qusr.isNull)
1263 			logInfo("User: %s", qusr.get.loginName);
1264 	}
1265 }
1266 
1267 /**
1268   Specifies a level of isolation for read operations. For example, you can use read concern to only read data that has propagated to a majority of nodes in a replica set.
1269 
1270   See_Also: $(LINK https://docs.mongodb.com/manual/reference/read-concern/)
1271  */
1272 struct ReadConcern {
1273 	///
1274 	enum Level : string {
1275 		/// This is the default read concern level.
1276 		local = "local",
1277 		/// This is the default for reads against secondaries when afterClusterTime and "level" are unspecified. The query returns the the instance’s most recent data.
1278 		available = "available",
1279 		/// Available for replica sets that use WiredTiger storage engine.
1280 		majority = "majority",
1281 		/// Available for read operations on the primary only.
1282 		linearizable = "linearizable"
1283 	}
1284 
1285 	/// The level of the read concern.
1286 	string level;
1287 }
1288 
1289 /**
1290   See_Also: $(LINK https://docs.mongodb.com/manual/reference/write-concern/)
1291  */
1292 struct WriteConcern {
1293 	/**
1294 		If true, wait for the the write operation to get committed to the
1295 
1296 		See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#journaled)
1297 	*/
1298 	@embedNullable @name("j")
1299 	Nullable!bool journal;
1300 
1301 	/**
1302 		When an integer, specifies the number of nodes that should acknowledge
1303 		the write and MUST be greater than or equal to 0.
1304 
1305 		When a string, indicates tags. "majority" is defined, but users could
1306 		specify other custom error modes.
1307 	*/
1308 	@embedNullable
1309 	Nullable!Bson w;
1310 
1311 	/**
1312 		If provided, and the write concern is not satisfied within the specified
1313 		timeout (in milliseconds), the server will return an error for the
1314 		operation.
1315 
1316 		See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#timeouts)
1317 	*/
1318 	@embedNullable @name("wtimeout")
1319 	Nullable!long wtimeoutMS;
1320 }
1321 
1322 /**
1323   Collation allows users to specify language-specific rules for string comparison, such as rules for letter-case and accent marks.
1324 
1325   See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation/)
1326  */
1327 struct Collation {
1328 	///
1329 	enum Alternate : string {
1330 		/// Whitespace and punctuation are considered base characters
1331 		nonIgnorable = "non-ignorable",
1332 		/// Whitespace and punctuation are not considered base characters and are only distinguished at strength levels greater than 3
1333 		shifted = "shifted",
1334 	}
1335 
1336 	///
1337 	enum MaxVariable : string {
1338 		/// Both whitespaces and punctuation are “ignorable”, i.e. not considered base characters.
1339 		punct = "punct",
1340 		/// Whitespace are “ignorable”, i.e. not considered base characters.
1341 		space = "space"
1342 	}
1343 
1344 	/**
1345 	  The ICU locale
1346 
1347 	  See_Also: See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation-locales-defaults/#collation-languages-locales) for a list of supported locales.
1348 
1349 	  To specify simple binary comparison, specify locale value of "simple".
1350 	 */
1351 	string locale;
1352 	/// The level of comparison to perform. Corresponds to ICU Comparison Levels.
1353 	@embedNullable Nullable!int strength;
1354 	/// Flag that determines whether to include case comparison at strength level 1 or 2.
1355 	@embedNullable Nullable!bool caseLevel;
1356 	/// A flag that determines sort order of case differences during tertiary level comparisons.
1357 	@embedNullable Nullable!string caseFirst;
1358 	/// Flag that determines whether to compare numeric strings as numbers or as strings.
1359 	@embedNullable Nullable!bool numericOrdering;
1360 	/// Field that determines whether collation should consider whitespace and punctuation as base characters for purposes of comparison.
1361 	@embedNullable Nullable!Alternate alternate;
1362 	/// Field that determines up to which characters are considered ignorable when `alternate: "shifted"`. Has no effect if `alternate: "non-ignorable"`
1363 	@embedNullable Nullable!MaxVariable maxVariable;
1364 	/**
1365 	  Flag that determines whether strings with diacritics sort from back of the string, such as with some French dictionary ordering.
1366 
1367 	  If `true` compare from back to front, otherwise front to back.
1368 	 */
1369 	@embedNullable Nullable!bool backwards;
1370 	/// Flag that determines whether to check if text require normalization and to perform normalization. Generally, majority of text does not require this normalization processing.
1371 	@embedNullable Nullable!bool normalization;
1372 }
1373 
1374 ///
1375 struct CursorInitArguments {
1376 	/// Specifies the initial batch size for the cursor. Or null for server
1377 	/// default value.
1378 	@embedNullable Nullable!int batchSize;
1379 }
1380 
1381 /// UDA to unset a nullable field if the server wire version doesn't at least
1382 /// match the given version. (inclusive)
1383 ///
1384 /// Use with $(LREF enforceWireVersionConstraints)
1385 struct MinWireVersion
1386 {
1387 	///
1388 	WireVersion v;
1389 }
1390 
1391 /// ditto
1392 MinWireVersion since(WireVersion v) @safe { return MinWireVersion(v); }
1393 
1394 /// UDA to warn when a nullable field is set and the server wire version matches
1395 /// the given version. (inclusive)
1396 ///
1397 /// Use with $(LREF enforceWireVersionConstraints)
1398 struct DeprecatedSinceWireVersion
1399 {
1400 	///
1401 	WireVersion v;
1402 }
1403 
1404 /// ditto
1405 DeprecatedSinceWireVersion deprecatedSince(WireVersion v) @safe { return DeprecatedSinceWireVersion(v); }
1406 
1407 /// UDA to throw a MongoException when a nullable field is set and the server
1408 /// wire version doesn't match the version. (inclusive)
1409 ///
1410 /// Use with $(LREF enforceWireVersionConstraints)
1411 struct ErrorBeforeWireVersion
1412 {
1413 	///
1414 	WireVersion v;
1415 }
1416 
1417 /// ditto
1418 ErrorBeforeWireVersion errorBefore(WireVersion v) @safe { return ErrorBeforeWireVersion(v); }
1419 
1420 /// UDA to unset a nullable field if the server wire version is newer than the
1421 /// given version. (inclusive)
1422 ///
1423 /// Use with $(LREF enforceWireVersionConstraints)
1424 struct MaxWireVersion
1425 {
1426 	///
1427 	WireVersion v;
1428 }
1429 /// ditto
1430 MaxWireVersion until(WireVersion v) @safe { return MaxWireVersion(v); }
1431 
1432 /// Unsets nullable fields not matching the server version as defined per UDAs.
1433 void enforceWireVersionConstraints(T)(ref T field, WireVersion serverVersion,
1434 	string file = __FILE__, size_t line = __LINE__)
1435 @safe {
1436 	import std.traits : getUDAs;
1437 
1438 	string exception;
1439 
1440 	foreach (i, ref v; field.tupleof) {
1441 		enum minV = getUDAs!(field.tupleof[i], MinWireVersion);
1442 		enum maxV = getUDAs!(field.tupleof[i], MaxWireVersion);
1443 		enum deprecateV = getUDAs!(field.tupleof[i], DeprecatedSinceWireVersion);
1444 		enum errorV = getUDAs!(field.tupleof[i], ErrorBeforeWireVersion);
1445 
1446 		static foreach (depr; deprecateV)
1447 			if (serverVersion >= depr.v && !v.isNull)
1448 				logInfo("User-set field '%s' is deprecated since MongoDB %s (from %s:%s)",
1449 					T.tupleof[i].stringof, depr.v, file, line);
1450 
1451 		static foreach (err; errorV)
1452 			if (serverVersion < err.v && !v.isNull)
1453 				exception ~= format("User-set field '%s' is not supported before MongoDB %s\n",
1454 					T.tupleof[i].stringof, err.v);
1455 
1456 		static foreach (min; minV)
1457 			if (serverVersion < min.v)
1458 				v.nullify();
1459 
1460 		static foreach (max; maxV)
1461 			if (serverVersion > max.v)
1462 				v.nullify();
1463 	}
1464 
1465 	if (exception.length)
1466 		throw new MongoException(exception ~ "from " ~ file ~ ":" ~ line.to!string);
1467 }
1468 
1469 ///
1470 @safe unittest
1471 {
1472 	struct SomeMongoCommand
1473 	{
1474 		@embedNullable @since(WireVersion.v34)
1475 		Nullable!int a;
1476 
1477 		@embedNullable @until(WireVersion.v30)
1478 		Nullable!int b;
1479 	}
1480 
1481 	SomeMongoCommand cmd;
1482 	cmd.a = 1;
1483 	cmd.b = 2;
1484 	assert(!cmd.a.isNull);
1485 	assert(!cmd.b.isNull);
1486 
1487 	SomeMongoCommand test = cmd;
1488 	enforceWireVersionConstraints(test, WireVersion.v30);
1489 	assert(test.a.isNull);
1490 	assert(!test.b.isNull);
1491 
1492 	test = cmd;
1493 	enforceWireVersionConstraints(test, WireVersion.v32);
1494 	assert(test.a.isNull);
1495 	assert(test.b.isNull);
1496 
1497 	test = cmd;
1498 	enforceWireVersionConstraints(test, WireVersion.v34);
1499 	assert(!test.a.isNull);
1500 	assert(test.b.isNull);
1501 }