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