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!R(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 	/// ditto
523 	MongoCursor!R find(R = Bson)(FindOptions options) { return find!R(Bson.emptyObject, options); }
524 
525 	///
526 	@safe unittest {
527 		import vibe.db.mongo.mongo;
528 
529 		void test()
530 		{
531 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
532 			// find all documents in the "test" collection.
533 			coll.find();
534 		}
535 	}
536 
537 	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.")
538 	auto findOne(R = Bson, T, U)(T query, U returnFieldSelector, QueryFlags flags)
539 	{
540 		import std.traits;
541 		import std.typecons;
542 
543 		auto c = find!R(query, returnFieldSelector, flags, 0, 1);
544 		static if (is(R == Bson)) {
545 			foreach (doc; c) return doc;
546 			return Bson(null);
547 		} else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) {
548 			foreach (doc; c) return doc;
549 			return null;
550 		} else {
551 			foreach (doc; c) {
552 				Nullable!R ret;
553 				ret = doc;
554 				return ret;
555 			}
556 			return Nullable!R.init;
557 		}
558 	}
559 
560 	/** Queries the collection for existing documents.
561 
562 		Returns:
563 			By default, a Bson value of the matching document is returned, or $(D Bson(null))
564 			when no document matched. For types R that are not Bson, the returned value is either
565 			of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type.
566 
567 			The projection parameter limits what fields are returned by the database,
568 			see projection documentation linked below.
569 
570 		Throws: Exception if a DB communication error or a query error occurred.
571 
572 		See_Also:
573 		- Querying: $(LINK http://www.mongodb.org/display/DOCS/Querying)
574 		- Projection: $(LINK https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#std-label-projections)
575 		- $(LREF find)
576 	 */
577 	auto findOne(R = Bson, T, U)(T query, U projection, FindOptions options = FindOptions.init)
578 	if (!is(U == FindOptions))
579 	{
580 		options.projection = serializeToBson(projection);
581 		return findOne!(R, T)(query, options);
582 	}
583 
584 	///
585 	@safe unittest {
586 		import vibe.db.mongo.mongo;
587 
588 		void test()
589 		{
590 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
591 			// find documents with status == "A"
592 			auto x = coll.findOne(["status": "A"], ["status": true, "otherField": true]);
593 			// x now only contains _id (implicit, unless you make it `false`), status and otherField
594 		}
595 	}
596 
597 	/** Queries the collection for existing documents.
598 
599 		Returns:
600 			By default, a Bson value of the matching document is returned, or $(D Bson(null))
601 			when no document matched. For types R that are not Bson, the returned value is either
602 			of type $(D R), or of type $(Nullable!R), if $(D R) is not a reference/pointer type.
603 
604 		Throws: Exception if a DB communication error or a query error occurred.
605 		See_Also:
606 		- $(LINK http://www.mongodb.org/display/DOCS/Querying)
607 		- $(LREF find)
608 	 */
609 	auto findOne(R = Bson, T)(T query, FindOptions options = FindOptions.init)
610 	{
611 		import std.traits;
612 		import std.typecons;
613 
614 		options.limit = 1;
615 		auto c = find!R(query, options);
616 		static if (is(R == Bson)) {
617 			foreach (doc; c) return doc;
618 			return Bson(null);
619 		} else static if (is(R == class) || isPointer!R || isDynamicArray!R || isAssociativeArray!R) {
620 			foreach (doc; c) return doc;
621 			return null;
622 		} else {
623 			foreach (doc; c) {
624 				Nullable!R ret;
625 				ret = doc;
626 				return ret;
627 			}
628 			return Nullable!R.init;
629 		}
630 	}
631 
632 	/**
633 	  Removes documents from the collection.
634 
635 	  Throws: Exception if a DB communication error occurred.
636 	  See_Also: $(LINK http://www.mongodb.org/display/DOCS/Removing)
637 	 */
638 	deprecated("Use `deleteOne` or `deleteMany` taking DeleteOptions instead, this method breaks in MongoDB 5.1 and onwards.")
639 	void remove(T)(T selector, DeleteFlags flags = DeleteFlags.None)
640 	{
641 		assert(m_client !is null, "Removing from uninitialized MongoCollection.");
642 		auto conn = m_client.lockConnection();
643 		ubyte[256] selector_buf = void;
644 		conn.delete_(m_fullPath, flags, serializeToBson(selector, selector_buf));
645 	}
646 
647 	/// ditto
648 	deprecated("Use `deleteMany` taking `DeleteOptions` instead, this method breaks in MongoDB 5.1 and onwards.")
649 	void remove()() { remove(Bson.emptyObject); }
650 
651 	/**
652 		Combines a modify and find operation to a single atomic operation.
653 
654 		Params:
655 			query = MongoDB query expression to identify the matched document
656 			update = Update expression for the matched document
657 			returnFieldSelector = Optional map of fields to return in the response
658 
659 		Throws:
660 			An `Exception` will be thrown if an error occurs in the
661 			communication with the database server.
662 
663 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify)
664 	 */
665 	Bson findAndModify(T, U, V)(T query, U update, V returnFieldSelector)
666 	{
667 		static struct CMD {
668 			string findAndModify;
669 			T query;
670 			U update;
671 			V fields;
672 		}
673 		CMD cmd;
674 		cmd.findAndModify = m_name;
675 		cmd.query = query;
676 		cmd.update = update;
677 		cmd.fields = returnFieldSelector;
678 		auto ret = database.runCommandChecked(cmd);
679 		return ret["value"];
680 	}
681 
682 	/// ditto
683 	Bson findAndModify(T, U)(T query, U update)
684 	{
685 		return findAndModify(query, update, null);
686 	}
687 
688 	/**
689 		Combines a modify and find operation to a single atomic operation with generic options support.
690 
691 		Params:
692 			query = MongoDB query expression to identify the matched document
693 			update = Update expression for the matched document
694 			options = Generic BSON object that contains additional options
695 				fields, such as `"new": true`
696 
697 		Throws:
698 			An `Exception` will be thrown if an error occurs in the
699 			communication with the database server.
700 
701 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/command/findAndModify)
702 	 */
703 	Bson findAndModifyExt(T, U, V)(T query, U update, V options)
704 	{
705 		auto bopt = serializeToBson(options);
706 		assert(bopt.type == Bson.Type.object,
707 			"The options parameter to findAndModifyExt must be a BSON object.");
708 
709 		Bson cmd = Bson.emptyObject;
710 		cmd["findAndModify"] = m_name;
711 		cmd["query"] = serializeToBson(query);
712 		cmd["update"] = serializeToBson(update);
713 		bopt.opApply(delegate int(string key, Bson value) @safe {
714 			cmd[key] = value;
715 			return 0;
716 		});
717 		auto ret = database.runCommandChecked(cmd);
718 		return ret["value"];
719 	}
720 
721 	///
722 	@safe unittest {
723 		import vibe.db.mongo.mongo;
724 
725 		void test()
726 		{
727 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
728 			coll.findAndModifyExt(["name": "foo"], ["$set": ["value": "bar"]], ["new": true]);
729 		}
730 	}
731 
732 	deprecated("deprecated since MongoDB v4.0, use countDocuments or estimatedDocumentCount instead")
733 	ulong count(T)(T query)
734 	{
735 		return countImpl!T(query);
736 	}
737 
738 	private ulong countImpl(T)(T query)
739 	{
740 		Bson cmd = Bson.emptyObject;
741 		cmd["count"] = m_name;
742 		cmd["query"] = serializeToBson(query);
743 		auto reply = database.runCommandChecked(cmd);
744 		switch (reply["n"].type) with (Bson.Type) {
745 			default: assert(false, "Unsupported data type in BSON reply for COUNT");
746 			case double_: return cast(ulong)reply["n"].get!double; // v2.x
747 			case int_: return reply["n"].get!int; // v3.x
748 			case long_: return reply["n"].get!long; // just in case
749 		}
750 	}
751 
752 	/**
753 		Returns the count of documents that match the query for a collection or
754 		view.
755 
756 		The method wraps the `$group` aggregation stage with a `$sum` expression
757 		to perform the count.
758 
759 		Throws Exception if a DB communication error occurred.
760 
761 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/)
762 	*/
763 	ulong countDocuments(T)(T filter, CountOptions options = CountOptions.init)
764 	{
765 		// https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details
766 		Bson[] pipeline = [Bson(["$match": serializeToBson(filter)])];
767 		if (!options.skip.isNull)
768 			pipeline ~= Bson(["$skip": Bson(options.skip.get)]);
769 		if (!options.limit.isNull)
770 			pipeline ~= Bson(["$limit": Bson(options.limit.get)]);
771 		pipeline ~= Bson(["$group": Bson([
772 			"_id": Bson(1),
773 			"n": Bson(["$sum": Bson(1)])
774 		])]);
775 		AggregateOptions aggOptions;
776 		foreach (i, field; options.tupleof)
777 		{
778 			enum name = CountOptions.tupleof[i].stringof;
779 			static if (name != "filter" && name != "skip" && name != "limit")
780 				__traits(getMember, aggOptions, name) = field;
781 		}
782 		auto reply = aggregate(pipeline, aggOptions);
783 		return reply.empty ? 0 : reply.front["n"].to!long;
784 	}
785 
786 	/**
787 		Returns the count of all documents in a collection or view.
788 
789 		Throws Exception if a DB communication error occurred.
790 
791 		See_Also: $(LINK https://www.mongodb.com/docs/manual/reference/method/db.collection.estimatedDocumentCount/)
792 	*/
793 	ulong estimatedDocumentCount(EstimatedDocumentCountOptions options = EstimatedDocumentCountOptions.init)
794 	{
795 		// https://github.com/mongodb/specifications/blob/525dae0aa8791e782ad9dd93e507b60c55a737bb/source/crud/crud.rst#count-api-details
796 		MongoConnection conn = m_client.lockConnection();
797 		if (conn.description.satisfiesVersion(WireVersion.v49)) {
798 			Bson[] pipeline = [
799 				Bson(["$collStats": Bson(["count": Bson.emptyObject])]),
800 				Bson(["$group": Bson([
801 					"_id": Bson(1),
802 					"n": Bson(["$sum": Bson("$count")])
803 				])])
804 			];
805 			AggregateOptions aggOptions;
806 			aggOptions.maxTimeMS = options.maxTimeMS;
807 			auto reply = aggregate(pipeline, aggOptions).front;
808 			return reply["n"].to!long;
809 		} else {
810 			return countImpl(null);
811 		}
812 	}
813 
814 	/**
815 		Calculates aggregate values for the data in a collection.
816 
817 		Params:
818 			pipeline = A sequence of data aggregation processes. These can
819 				either be given as separate parameters, or as a single array
820 				parameter.
821 
822 		Returns:
823 			Returns the list of documents aggregated by the pipeline. The return
824 			value is either a single `Bson` array value or a `MongoCursor`
825 			(input range) of the requested document type.
826 
827 		Throws: Exception if a DB communication error occurred.
828 
829 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/db.collection.aggregate)
830 	*/
831 	Bson aggregate(ARGS...)(ARGS pipeline) @safe
832 	{
833 		import std.traits : isArray;
834 
835 		static if (ARGS.length == 1 && isArray!(ARGS[0]))
836 			auto convPipeline = pipeline;
837 		else {
838 			static struct Pipeline { @asArray ARGS pipeline; }
839 
840 			Bson[] convPipeline = serializeToBson(Pipeline(pipeline))["pipeline"].get!(Bson[]);
841 		}
842 
843 		return aggregate(convPipeline, AggregateOptions.init).array.serializeToBson;
844 	}
845 
846 	/// ditto
847 	MongoCursor!R aggregate(R = Bson, S = Bson)(S[] pipeline, AggregateOptions options) @safe
848 	{
849 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
850 
851 		Bson cmd = Bson.emptyObject; // empty object because order is important
852 		cmd["aggregate"] = Bson(m_name);
853 		cmd["$db"] = Bson(m_db.name);
854 		cmd["pipeline"] = serializeToBson(pipeline);
855 		MongoConnection conn = m_client.lockConnection();
856 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
857 		foreach (string k, v; serializeToBson(options).byKeyValue)
858 		{
859 			// spec recommends to omit cursor field when explain is true
860 			if (!options.explain.isNull && options.explain.get && k == "cursor")
861 				continue;
862 			cmd[k] = v;
863 		}
864 		return MongoCursor!R(m_client, cmd,
865 			!options.batchSize.isNull ? options.batchSize.get : 0,
866 			!options.maxAwaitTimeMS.isNull ? options.maxAwaitTimeMS.get.msecs
867 				: !options.maxTimeMS.isNull ? options.maxTimeMS.get.msecs
868 				: Duration.max);
869 	}
870 
871 	/// Example taken from the MongoDB documentation
872 	@safe unittest {
873 		import vibe.db.mongo.mongo;
874 
875 		void test() {
876 			auto db = connectMongoDB("127.0.0.1").getDatabase("test");
877 			auto results = db["coll"].aggregate(
878 				["$match": ["status": "A"]],
879 				["$group": ["_id": Bson("$cust_id"),
880 					"total": Bson(["$sum": Bson("$amount")])]],
881 				["$sort": ["total": -1]]);
882 		}
883 	}
884 
885 	/// The same example, but using an array of arguments with custom options
886 	@safe unittest {
887 		import vibe.db.mongo.mongo;
888 
889 		void test() {
890 			auto db = connectMongoDB("127.0.0.1").getDatabase("test");
891 
892 			Bson[] args;
893 			args ~= serializeToBson(["$match": ["status": "A"]]);
894 			args ~= serializeToBson(["$group": ["_id": Bson("$cust_id"),
895 					"total": Bson(["$sum": Bson("$amount")])]]);
896 			args ~= serializeToBson(["$sort": ["total": -1]]);
897 
898 			AggregateOptions options;
899 			options.cursor.batchSize = 10; // pre-fetch the first 10 results
900 			auto results = db["coll"].aggregate(args, options);
901 		}
902 	}
903 
904 	/**
905 		Returns an input range of all unique values for a certain field for
906 		records matching the given query.
907 
908 		Params:
909 			fieldName = Name of the field for which to collect unique values
910 			query = The query used to select records
911 			options = Options to apply
912 
913 		Returns:
914 			An input range with items of type `R` (`Bson` by default) is
915 			returned.
916 	*/
917 	auto distinct(R = Bson, Q)(string fieldName, Q query, DistinctOptions options = DistinctOptions.init)
918 	{
919 		assert(m_client !is null, "Querying uninitialized MongoCollection.");
920 
921 		Bson cmd = Bson.emptyObject; // empty object because order is important
922 		cmd["distinct"] = Bson(m_name);
923 		cmd["key"] = Bson(fieldName);
924 		cmd["query"] = serializeToBson(query);
925 		MongoConnection conn = m_client.lockConnection();
926 		enforceWireVersionConstraints(options, conn.description.maxWireVersion);
927 		foreach (string k, v; serializeToBson(options).byKeyValue)
928 			cmd[k] = v;
929 
930 		import std.algorithm : map;
931 
932 		auto res = m_db.runCommandChecked(cmd);
933 		static if (is(R == Bson)) return res["values"].byValue;
934 		else return res["values"].byValue.map!(b => deserializeBson!R(b));
935 	}
936 
937 	///
938 	@safe unittest {
939 		import std.algorithm : equal;
940 		import vibe.db.mongo.mongo;
941 
942 		void test()
943 		{
944 			auto db = connectMongoDB("127.0.0.1").getDatabase("test");
945 			auto coll = db["collection"];
946 
947 			coll.drop();
948 			coll.insertOne(["a": "first", "b": "foo"]);
949 			coll.insertOne(["a": "first", "b": "bar"]);
950 			coll.insertOne(["a": "first", "b": "bar"]);
951 			coll.insertOne(["a": "second", "b": "baz"]);
952 			coll.insertOne(["a": "second", "b": "bam"]);
953 
954 			auto result = coll.distinct!string("b", ["a": "first"]);
955 
956 			assert(result.equal(["foo", "bar"]));
957 		}
958 	}
959 
960 	/*
961 		following MongoDB standard API for the Index Management specification:
962 
963 		Standards: https://github.com/mongodb/specifications/blob/0c6e56141c867907aacf386e0cbe56d6562a0614/source/index-management.rst#standard-api
964 	*/
965 
966 	deprecated("This is a legacy API, call createIndexes instead")
967 	void ensureIndex(scope const(Tuple!(string, int))[] field_orders, IndexFlags flags = IndexFlags.none, Duration expire_time = 0.seconds)
968 	@safe {
969 		IndexModel[1] models;
970 		IndexOptions options;
971 		if (flags & IndexFlags.unique) options.unique = true;
972 		if (flags & IndexFlags.dropDuplicates) options.dropDups = true;
973 		if (flags & IndexFlags.background) options.background = true;
974 		if (flags & IndexFlags.sparse) options.sparse = true;
975 		if (flags & IndexFlags.expireAfterSeconds) options.expireAfter = expire_time;
976 
977 		models[0].options = options;
978 		foreach (field; field_orders) {
979 			models[0].add(field[0], field[1]);
980 		}
981 		createIndexes(models);
982 	}
983 
984 	deprecated("This is a legacy API, call createIndexes instead. This API is not recommended to be used because of unstable dictionary ordering.")
985 	void ensureIndex(int[string] field_orders, IndexFlags flags = IndexFlags.none, ulong expireAfterSeconds = 0)
986 	@safe {
987 		Tuple!(string, int)[] orders;
988 		foreach (k, v; field_orders)
989 			orders ~= tuple(k, v);
990 		ensureIndex(orders, flags, expireAfterSeconds.seconds);
991 	}
992 
993 	/**
994 		Drops a single index from the collection by the index name.
995 
996 		Throws: `Exception` if it is attempted to pass in `*`.
997 		Use dropIndexes() to remove all indexes instead.
998 	*/
999 	void dropIndex(string name, DropIndexOptions options = DropIndexOptions.init)
1000 	@safe {
1001 		if (name == "*")
1002 			throw new Exception("Attempted to remove single index with '*'");
1003 
1004 		static struct CMD {
1005 			string dropIndexes;
1006 			string index;
1007 		}
1008 
1009 		CMD cmd;
1010 		cmd.dropIndexes = m_name;
1011 		cmd.index = name;
1012 		database.runCommandChecked(cmd);
1013 	}
1014 
1015 	/// ditto
1016 	void dropIndex(T)(T keys,
1017 		IndexOptions indexOptions = IndexOptions.init,
1018 		DropIndexOptions options = DropIndexOptions.init)
1019 	@safe if (!is(Unqual!T == IndexModel))
1020 	{
1021 		IndexModel model;
1022 		model.keys = serializeToBson(keys);
1023 		model.options = indexOptions;
1024 		dropIndex(model.name, options);
1025 	}
1026 
1027 	/// ditto
1028 	void dropIndex(const IndexModel keys,
1029 		DropIndexOptions options = DropIndexOptions.init)
1030 	@safe {
1031 		dropIndex(keys.name, options);
1032 	}
1033 
1034 	///
1035 	@safe unittest
1036 	{
1037 		import vibe.db.mongo.mongo;
1038 
1039 		void test()
1040 		{
1041 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1042 			auto primarykey = IndexModel()
1043 					.add("name", 1)
1044 					.add("primarykey", -1);
1045 			coll.dropIndex(primarykey);
1046 		}
1047 	}
1048 
1049 	/// Drops all indexes in the collection.
1050 	void dropIndexes(DropIndexOptions options = DropIndexOptions.init)
1051 	@safe {
1052 		static struct CMD {
1053 			string dropIndexes;
1054 			string index;
1055 		}
1056 
1057 		CMD cmd;
1058 		cmd.dropIndexes = m_name;
1059 		cmd.index = "*";
1060 		database.runCommandChecked(cmd);
1061 	}
1062 
1063 	/// Unofficial API extension, more efficient multi-index removal on
1064 	/// MongoDB 4.2+
1065 	void dropIndexes(string[] names, DropIndexOptions options = DropIndexOptions.init)
1066 	@safe {
1067 		MongoConnection conn = m_client.lockConnection();
1068 		if (conn.description.satisfiesVersion(WireVersion.v42)) {
1069 			static struct CMD {
1070 				string dropIndexes;
1071 				string[] index;
1072 			}
1073 
1074 			CMD cmd;
1075 			cmd.dropIndexes = m_name;
1076 			cmd.index = names;
1077 			database.runCommandChecked(cmd);
1078 		} else {
1079 			foreach (name; names)
1080 				dropIndex(name);
1081 		}
1082 	}
1083 
1084 	///
1085 	@safe unittest
1086 	{
1087 		import vibe.db.mongo.mongo;
1088 
1089 		void test()
1090 		{
1091 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1092 			coll.dropIndexes(["name_1_primarykey_-1"]);
1093 		}
1094 	}
1095 
1096 	/**
1097 		Convenience method for creating a single index. Calls `createIndexes`
1098 
1099 		Supports any kind of document for template parameter T or a IndexModel.
1100 
1101 		Params:
1102 			keys = a IndexModel or type with integer or string fields indicating
1103 				index direction or index type.
1104 	*/
1105 	string createIndex(T)(T keys,
1106 		IndexOptions indexOptions = IndexOptions.init,
1107 		CreateIndexOptions options = CreateIndexOptions.init)
1108 	@safe if (!is(Unqual!T == IndexModel))
1109 	{
1110 		IndexModel[1] model;
1111 		model[0].keys = serializeToBson(keys);
1112 		model[0].options = indexOptions;
1113 		return createIndexes(model[], options)[0];
1114 	}
1115 
1116 	/// ditto
1117 	string createIndex(const IndexModel keys,
1118 		CreateIndexOptions options = CreateIndexOptions.init)
1119 	@safe {
1120 		IndexModel[1] model;
1121 		model[0] = keys;
1122 		return createIndexes(model[], options)[0];
1123 	}
1124 
1125 	///
1126 	@safe unittest
1127 	{
1128 		import vibe.db.mongo.mongo;
1129 
1130 		void test()
1131 		{
1132 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1133 
1134 			// simple ascending name, descending primarykey compound-index
1135 			coll.createIndex(["name": 1, "primarykey": -1]);
1136 
1137 			IndexOptions textOptions = {
1138 				// pick language from another field called "idioma"
1139 				languageOverride: "idioma"
1140 			};
1141 			auto textIndex = IndexModel()
1142 					.withOptions(textOptions)
1143 					.add("comments", IndexType.text);
1144 			// more complex text index in DB with independent language
1145 			coll.createIndex(textIndex);
1146 		}
1147 	}
1148 
1149 	/**
1150 		Builds one or more indexes in the collection.
1151 
1152 		See_Also: $(LINK https://docs.mongodb.com/manual/reference/command/createIndexes/)
1153 	*/
1154 	string[] createIndexes(scope const(IndexModel)[] models,
1155 		CreateIndexesOptions options = CreateIndexesOptions.init,
1156 		string file = __FILE__, size_t line = __LINE__)
1157 	@safe {
1158 		string[] keys = new string[models.length];
1159 
1160 		MongoConnection conn = m_client.lockConnection();
1161 		if (conn.description.satisfiesVersion(WireVersion.v26)) {
1162 			Bson cmd = Bson.emptyObject;
1163 			cmd["createIndexes"] = m_name;
1164 			Bson[] indexes;
1165 			foreach (model; models) {
1166 				// trusted to support old compilers which think opt_dup has
1167 				// longer lifetime than model.options
1168 				IndexOptions opt_dup = (() @trusted => model.options)();
1169 				enforceWireVersionConstraints(opt_dup, conn.description.maxWireVersion, file, line);
1170 				Bson index = serializeToBson(opt_dup);
1171 				index["key"] = model.keys;
1172 				index["name"] = model.name;
1173 				indexes ~= index;
1174 			}
1175 			cmd["indexes"] = Bson(indexes);
1176 			database.runCommandChecked(cmd);
1177 		} else {
1178 			foreach (model; models) {
1179 				// trusted to support old compilers which think opt_dup has
1180 				// longer lifetime than model.options
1181 				IndexOptions opt_dup = (() @trusted => model.options)();
1182 				enforceWireVersionConstraints(opt_dup, WireVersion.old, file, line);
1183 				Bson doc = serializeToBson(opt_dup);
1184 				doc["v"] = 1;
1185 				doc["key"] = model.keys;
1186 				doc["ns"] = m_fullPath;
1187 				doc["name"] = model.name;
1188 				database["system.indexes"].insertOne(doc);
1189 			}
1190 		}
1191 
1192 		return keys;
1193 	}
1194 
1195 	/**
1196 		Returns an array that holds a list of documents that identify and describe the existing indexes on the collection.
1197 	*/
1198 	MongoCursor!R listIndexes(R = Bson)()
1199 	@safe {
1200 		MongoConnection conn = m_client.lockConnection();
1201 		if (conn.description.satisfiesVersion(WireVersion.v30)) {
1202 			Bson command = Bson.emptyObject;
1203 			command["listIndexes"] = Bson(m_name);
1204 			command["$db"] = Bson(m_db.name);
1205 			return MongoCursor!R(m_client, command);
1206 		} else {
1207 			throw new MongoDriverException("listIndexes not supported on MongoDB <3.0");
1208 		}
1209 	}
1210 
1211 	///
1212 	@safe unittest
1213 	{
1214 		import vibe.db.mongo.mongo;
1215 
1216 		void test()
1217 		{
1218 			auto coll = connectMongoDB("127.0.0.1").getCollection("test");
1219 
1220 			foreach (index; coll.listIndexes())
1221 				logInfo("index %s: %s", index["name"].get!string, index);
1222 		}
1223 	}
1224 
1225 	deprecated("Please use the standard API name 'listIndexes'") alias getIndexes = listIndexes;
1226 
1227 	/**
1228 		Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection.
1229 	*/
1230 	void drop()
1231 	@safe {
1232 		static struct CMD {
1233 			string drop;
1234 		}
1235 
1236 		CMD cmd;
1237 		cmd.drop = m_name;
1238 		auto reply = database.runCommandUnchecked(cmd);
1239 		if (reply["ok"].get!double != 1.0) {
1240 			auto code = reply["code"].opt!int(0);
1241 			if (code != 26) // NamespaceNotFound
1242 				throw new MongoDriverException(
1243 					"command failed: " ~ reply["errmsg"].opt!string("(no message)"));
1244 		}
1245 	}
1246 }
1247 
1248 ///
1249 @safe unittest {
1250 	import vibe.data.bson;
1251 	import vibe.data.json;
1252 	import vibe.db.mongo.mongo;
1253 
1254 	void test()
1255 	{
1256 		MongoClient client = connectMongoDB("127.0.0.1");
1257 		MongoCollection users = client.getCollection("myapp.users");
1258 
1259 		// canonical version using a Bson object
1260 		users.insertOne(Bson(["name": Bson("admin"), "password": Bson("secret")]));
1261 
1262 		// short version using a string[string] AA that is automatically
1263 		// serialized to Bson
1264 		users.insertOne(["name": "admin", "password": "secret"]);
1265 
1266 		// BSON specific types are also serialized automatically
1267 		auto uid = BsonObjectID.fromString("507f1f77bcf86cd799439011");
1268 		Bson usr = users.findOne(["_id": uid]);
1269 
1270 		// JSON is another possibility
1271 		Json jusr = parseJsonString(`{"name": "admin", "password": "secret"}`);
1272 		users.insertOne(jusr);
1273 	}
1274 }
1275 
1276 /// Using the type system to define a document "schema"
1277 @safe unittest {
1278 	import vibe.db.mongo.mongo;
1279 	import vibe.data.serialization : name;
1280 	import std.typecons : Nullable;
1281 
1282 	// Nested object within a "User" document
1283 	struct Address {
1284 		string name;
1285 		string street;
1286 		int zipCode;
1287 	}
1288 
1289 	// The document structure of the "myapp.users" collection
1290 	struct User {
1291 		@name("_id") BsonObjectID id; // represented as "_id" in the database
1292 		string loginName;
1293 		string password;
1294 		Address address;
1295 	}
1296 
1297 	void test()
1298 	{
1299 		MongoClient client = connectMongoDB("127.0.0.1");
1300 		MongoCollection users = client.getCollection("myapp.users");
1301 
1302 		// D values are automatically serialized to the internal BSON format
1303 		// upon insertion - see also vibe.data.serialization
1304 		User usr;
1305 		usr.id = BsonObjectID.generate();
1306 		usr.loginName = "admin";
1307 		usr.password = "secret";
1308 		users.insertOne(usr);
1309 
1310 		// find supports direct de-serialization of the returned documents
1311 		foreach (usr2; users.find!User()) {
1312 			logInfo("User: %s", usr2.loginName);
1313 		}
1314 
1315 		// the same goes for findOne
1316 		Nullable!User qusr = users.findOne!User(["_id": usr.id]);
1317 		if (!qusr.isNull)
1318 			logInfo("User: %s", qusr.get.loginName);
1319 	}
1320 }
1321 
1322 /**
1323   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.
1324 
1325   See_Also: $(LINK https://docs.mongodb.com/manual/reference/read-concern/)
1326  */
1327 struct ReadConcern {
1328 	///
1329 	enum Level : string {
1330 		/// This is the default read concern level.
1331 		local = "local",
1332 		/// This is the default for reads against secondaries when afterClusterTime and "level" are unspecified. The query returns the the instance’s most recent data.
1333 		available = "available",
1334 		/// Available for replica sets that use WiredTiger storage engine.
1335 		majority = "majority",
1336 		/// Available for read operations on the primary only.
1337 		linearizable = "linearizable"
1338 	}
1339 
1340 	/// The level of the read concern.
1341 	string level;
1342 }
1343 
1344 /**
1345   See_Also: $(LINK https://docs.mongodb.com/manual/reference/write-concern/)
1346  */
1347 struct WriteConcern {
1348 	/**
1349 		If true, wait for the the write operation to get committed to the
1350 
1351 		See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#journaled)
1352 	*/
1353 	@embedNullable @name("j")
1354 	Nullable!bool journal;
1355 
1356 	/**
1357 		When an integer, specifies the number of nodes that should acknowledge
1358 		the write and MUST be greater than or equal to 0.
1359 
1360 		When a string, indicates tags. "majority" is defined, but users could
1361 		specify other custom error modes.
1362 	*/
1363 	@embedNullable
1364 	Nullable!Bson w;
1365 
1366 	/**
1367 		If provided, and the write concern is not satisfied within the specified
1368 		timeout (in milliseconds), the server will return an error for the
1369 		operation.
1370 
1371 		See_Also: $(LINK http://docs.mongodb.org/manual/core/write-concern/#timeouts)
1372 	*/
1373 	@embedNullable @name("wtimeout")
1374 	Nullable!long wtimeoutMS;
1375 }
1376 
1377 /**
1378   Collation allows users to specify language-specific rules for string comparison, such as rules for letter-case and accent marks.
1379 
1380   See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation/)
1381  */
1382 struct Collation {
1383 	///
1384 	enum Alternate : string {
1385 		/// Whitespace and punctuation are considered base characters
1386 		nonIgnorable = "non-ignorable",
1387 		/// Whitespace and punctuation are not considered base characters and are only distinguished at strength levels greater than 3
1388 		shifted = "shifted",
1389 	}
1390 
1391 	///
1392 	enum MaxVariable : string {
1393 		/// Both whitespaces and punctuation are “ignorable”, i.e. not considered base characters.
1394 		punct = "punct",
1395 		/// Whitespace are “ignorable”, i.e. not considered base characters.
1396 		space = "space"
1397 	}
1398 
1399 	/**
1400 	  The ICU locale
1401 
1402 	  See_Also: See_Also: $(LINK https://docs.mongodb.com/manual/reference/collation-locales-defaults/#collation-languages-locales) for a list of supported locales.
1403 
1404 	  To specify simple binary comparison, specify locale value of "simple".
1405 	 */
1406 	string locale;
1407 	/// The level of comparison to perform. Corresponds to ICU Comparison Levels.
1408 	@embedNullable Nullable!int strength;
1409 	/// Flag that determines whether to include case comparison at strength level 1 or 2.
1410 	@embedNullable Nullable!bool caseLevel;
1411 	/// A flag that determines sort order of case differences during tertiary level comparisons.
1412 	@embedNullable Nullable!string caseFirst;
1413 	/// Flag that determines whether to compare numeric strings as numbers or as strings.
1414 	@embedNullable Nullable!bool numericOrdering;
1415 	/// Field that determines whether collation should consider whitespace and punctuation as base characters for purposes of comparison.
1416 	@embedNullable Nullable!Alternate alternate;
1417 	/// Field that determines up to which characters are considered ignorable when `alternate: "shifted"`. Has no effect if `alternate: "non-ignorable"`
1418 	@embedNullable Nullable!MaxVariable maxVariable;
1419 	/**
1420 	  Flag that determines whether strings with diacritics sort from back of the string, such as with some French dictionary ordering.
1421 
1422 	  If `true` compare from back to front, otherwise front to back.
1423 	 */
1424 	@embedNullable Nullable!bool backwards;
1425 	/// Flag that determines whether to check if text require normalization and to perform normalization. Generally, majority of text does not require this normalization processing.
1426 	@embedNullable Nullable!bool normalization;
1427 }
1428 
1429 ///
1430 struct CursorInitArguments {
1431 	/// Specifies the initial batch size for the cursor. Or null for server
1432 	/// default value.
1433 	@embedNullable Nullable!int batchSize;
1434 }
1435 
1436 /// UDA to unset a nullable field if the server wire version doesn't at least
1437 /// match the given version. (inclusive)
1438 ///
1439 /// Use with $(LREF enforceWireVersionConstraints)
1440 struct MinWireVersion
1441 {
1442 	///
1443 	WireVersion v;
1444 }
1445 
1446 /// ditto
1447 MinWireVersion since(WireVersion v) @safe { return MinWireVersion(v); }
1448 
1449 /// UDA to warn when a nullable field is set and the server wire version matches
1450 /// the given version. (inclusive)
1451 ///
1452 /// Use with $(LREF enforceWireVersionConstraints)
1453 struct DeprecatedSinceWireVersion
1454 {
1455 	///
1456 	WireVersion v;
1457 }
1458 
1459 /// ditto
1460 DeprecatedSinceWireVersion deprecatedSince(WireVersion v) @safe { return DeprecatedSinceWireVersion(v); }
1461 
1462 /// UDA to throw a MongoException when a nullable field is set and the server
1463 /// wire version doesn't match the version. (inclusive)
1464 ///
1465 /// Use with $(LREF enforceWireVersionConstraints)
1466 struct ErrorBeforeWireVersion
1467 {
1468 	///
1469 	WireVersion v;
1470 }
1471 
1472 /// ditto
1473 ErrorBeforeWireVersion errorBefore(WireVersion v) @safe { return ErrorBeforeWireVersion(v); }
1474 
1475 /// UDA to unset a nullable field if the server wire version is newer than the
1476 /// given version. (inclusive)
1477 ///
1478 /// Use with $(LREF enforceWireVersionConstraints)
1479 struct MaxWireVersion
1480 {
1481 	///
1482 	WireVersion v;
1483 }
1484 /// ditto
1485 MaxWireVersion until(WireVersion v) @safe { return MaxWireVersion(v); }
1486 
1487 /// Unsets nullable fields not matching the server version as defined per UDAs.
1488 void enforceWireVersionConstraints(T)(ref T field, int serverVersion,
1489 	string file = __FILE__, size_t line = __LINE__)
1490 @safe {
1491 	import std.traits : getUDAs;
1492 
1493 	string exception;
1494 
1495 	foreach (i, ref v; field.tupleof) {
1496 		enum minV = getUDAs!(field.tupleof[i], MinWireVersion);
1497 		enum maxV = getUDAs!(field.tupleof[i], MaxWireVersion);
1498 		enum deprecateV = getUDAs!(field.tupleof[i], DeprecatedSinceWireVersion);
1499 		enum errorV = getUDAs!(field.tupleof[i], ErrorBeforeWireVersion);
1500 
1501 		static foreach (depr; deprecateV)
1502 			if (serverVersion >= depr.v && !v.isNull)
1503 				logInfo("User-set field '%s' is deprecated since MongoDB %s (from %s:%s)",
1504 					T.tupleof[i].stringof, depr.v, file, line);
1505 
1506 		static foreach (err; errorV)
1507 			if (serverVersion < err.v && !v.isNull)
1508 				exception ~= format("User-set field '%s' is not supported before MongoDB %s\n",
1509 					T.tupleof[i].stringof, err.v);
1510 
1511 		static foreach (min; minV)
1512 			if (serverVersion < min.v)
1513 				v.nullify();
1514 
1515 		static foreach (max; maxV)
1516 			if (serverVersion > max.v)
1517 				v.nullify();
1518 	}
1519 
1520 	if (exception.length)
1521 		throw new MongoException(exception ~ "from " ~ file ~ ":" ~ line.to!string);
1522 }
1523 
1524 version (unittest)
1525 {
1526 	struct SinceUntilCmd
1527 	{
1528 		@embedNullable @since(WireVersion.v34)
1529 		Nullable!int a;
1530 
1531 		@embedNullable @until(WireVersion.v30)
1532 		Nullable!int b;
1533 	}
1534 
1535 	struct ErrorBeforeCmd
1536 	{
1537 		@embedNullable @errorBefore(WireVersion.v44)
1538 		Nullable!int field;
1539 	}
1540 
1541 	struct DeprecatedCmd
1542 	{
1543 		@embedNullable @deprecatedSince(WireVersion.v40)
1544 		Nullable!int oldField;
1545 	}
1546 
1547 	struct CombinedCmd
1548 	{
1549 		@embedNullable @errorBefore(WireVersion.v44)
1550 		Nullable!bool allowDiskUse;
1551 
1552 		@embedNullable @since(WireVersion.v32)
1553 		Nullable!long maxAwaitTimeMS;
1554 
1555 		@embedNullable @deprecatedSince(WireVersion.v40)
1556 		Nullable!long maxScan;
1557 	}
1558 
1559 	struct SinceDeprecatedCmd
1560 	{
1561 		@embedNullable @since(WireVersion.v32)
1562 		Nullable!long maxAwaitTimeMS;
1563 
1564 		@embedNullable @deprecatedSince(WireVersion.v40)
1565 		Nullable!long maxScan;
1566 	}
1567 }
1568 
1569 /// @since nullifies field when server version is below minimum
1570 @safe unittest
1571 {
1572 	SinceUntilCmd cmd;
1573 	cmd.a = 1;
1574 	cmd.b = 2;
1575 
1576 	auto test = cmd;
1577 	enforceWireVersionConstraints(test, WireVersion.v30);
1578 	assert(test.a.isNull);
1579 	assert(!test.b.isNull);
1580 }
1581 
1582 /// @until nullifies field when server version exceeds maximum
1583 @safe unittest
1584 {
1585 	SinceUntilCmd cmd;
1586 	cmd.a = 1;
1587 	cmd.b = 2;
1588 
1589 	auto test = cmd;
1590 	enforceWireVersionConstraints(test, WireVersion.v32);
1591 	assert(test.a.isNull);
1592 	assert(test.b.isNull);
1593 }
1594 
1595 /// @since preserves field when server version meets minimum
1596 @safe unittest
1597 {
1598 	SinceUntilCmd cmd;
1599 	cmd.a = 1;
1600 	cmd.b = 2;
1601 
1602 	auto test = cmd;
1603 	enforceWireVersionConstraints(test, WireVersion.v34);
1604 	assert(!test.a.isNull);
1605 	assert(test.b.isNull);
1606 }
1607 
1608 /// @errorBefore throws when field is set and server version is below threshold
1609 @safe unittest
1610 {
1611 	ErrorBeforeCmd cmd;
1612 	cmd.field = 42;
1613 	try {
1614 		enforceWireVersionConstraints(cmd, WireVersion.v40);
1615 		assert(false, "Should have thrown");
1616 	} catch (MongoException e) {
1617 		// expected
1618 	}
1619 }
1620 
1621 /// @errorBefore does not throw when field is set and server version is at threshold
1622 @safe unittest
1623 {
1624 	ErrorBeforeCmd cmd;
1625 	cmd.field = 42;
1626 	enforceWireVersionConstraints(cmd, WireVersion.v44);
1627 	assert(!cmd.field.isNull);
1628 }
1629 
1630 /// @errorBefore does not throw when field is set and server version is above threshold
1631 @safe unittest
1632 {
1633 	ErrorBeforeCmd cmd;
1634 	cmd.field = 42;
1635 	enforceWireVersionConstraints(cmd, WireVersion.v60);
1636 	assert(!cmd.field.isNull);
1637 }
1638 
1639 /// @errorBefore does not throw when field is not set
1640 @safe unittest
1641 {
1642 	ErrorBeforeCmd cmd;
1643 	enforceWireVersionConstraints(cmd, WireVersion.v30);
1644 	assert(cmd.field.isNull);
1645 }
1646 
1647 /// @deprecatedSince preserves field and only logs at deprecated version
1648 @safe unittest
1649 {
1650 	DeprecatedCmd cmd;
1651 	cmd.oldField = 10;
1652 	enforceWireVersionConstraints(cmd, WireVersion.v40);
1653 	assert(!cmd.oldField.isNull);
1654 	assert(cmd.oldField.get == 10);
1655 }
1656 
1657 /// @deprecatedSince preserves field above deprecated version
1658 @safe unittest
1659 {
1660 	DeprecatedCmd cmd;
1661 	cmd.oldField = 10;
1662 	enforceWireVersionConstraints(cmd, WireVersion.v60);
1663 	assert(!cmd.oldField.isNull);
1664 }
1665 
1666 /// @deprecatedSince preserves field below deprecated version without warning
1667 @safe unittest
1668 {
1669 	DeprecatedCmd cmd;
1670 	cmd.oldField = 10;
1671 	enforceWireVersionConstraints(cmd, WireVersion.v36);
1672 	assert(!cmd.oldField.isNull);
1673 }
1674 
1675 /// @deprecatedSince does nothing when field is not set
1676 @safe unittest
1677 {
1678 	DeprecatedCmd cmd;
1679 	enforceWireVersionConstraints(cmd, WireVersion.v60);
1680 	assert(cmd.oldField.isNull);
1681 }
1682 
1683 /// Combined UDAs: @errorBefore throws while @since and @deprecatedSince still apply
1684 @safe unittest
1685 {
1686 	CombinedCmd cmd;
1687 	cmd.allowDiskUse = true;
1688 	cmd.maxAwaitTimeMS = 5000;
1689 	cmd.maxScan = 100;
1690 
1691 	auto t1 = cmd;
1692 	try {
1693 		enforceWireVersionConstraints(t1, WireVersion.v30);
1694 		assert(false, "Should have thrown due to errorBefore(v44)");
1695 	} catch (MongoException e) {
1696 		// expected
1697 	}
1698 }
1699 
1700 /// Combined UDAs: all fields valid at v44, @deprecatedSince only logs
1701 @safe unittest
1702 {
1703 	CombinedCmd cmd;
1704 	cmd.allowDiskUse = true;
1705 	cmd.maxAwaitTimeMS = 5000;
1706 	cmd.maxScan = 100;
1707 
1708 	enforceWireVersionConstraints(cmd, WireVersion.v44);
1709 	assert(!cmd.allowDiskUse.isNull);
1710 	assert(!cmd.maxAwaitTimeMS.isNull);
1711 	assert(!cmd.maxScan.isNull);
1712 }
1713 
1714 /// Combined UDAs: @since nullifies field below minimum while others are independent
1715 @safe unittest
1716 {
1717 	SinceDeprecatedCmd cmd;
1718 	cmd.maxAwaitTimeMS = 5000;
1719 	cmd.maxScan = 100;
1720 
1721 	enforceWireVersionConstraints(cmd, WireVersion.v30);
1722 	assert(cmd.maxAwaitTimeMS.isNull);
1723 	assert(!cmd.maxScan.isNull);
1724 }
1725 
1726 /// Combined UDAs: @since preserves field at sufficient version
1727 @safe unittest
1728 {
1729 	SinceDeprecatedCmd cmd;
1730 	cmd.maxAwaitTimeMS = 5000;
1731 	cmd.maxScan = 100;
1732 
1733 	enforceWireVersionConstraints(cmd, WireVersion.v34);
1734 	assert(!cmd.maxAwaitTimeMS.isNull);
1735 	assert(!cmd.maxScan.isNull);
1736 }