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